archive-org-mcp 1.0.0

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 ADDED
@@ -0,0 +1,25 @@
1
+ # archive-org-mcp
2
+
3
+ Internet Archive search, item metadata, and Wayback snapshots.
4
+
5
+ A merged MCP server that consolidates duplicate single-purpose servers in this monorepo into one focused server.
6
+
7
+ ## Tools
8
+
9
+ - `searchItems` — Search the Internet Archive.
10
+ - `getItem` — Details and file manifest for an item.
11
+ - `getSnapshots` — Wayback snapshot history for a URL.
12
+ - `getAvailability` — Closest Wayback snapshot for a URL.
13
+ - `readSnapshot` — Read archived page text.
14
+
15
+ ## Run
16
+
17
+ ```bash
18
+ npm install
19
+ npm run build
20
+ node dist/index.js
21
+ ```
22
+
23
+ ## Source
24
+
25
+ Public free APIs only. See `src/api.ts` for exact endpoints.
package/dist/api.d.ts ADDED
@@ -0,0 +1,122 @@
1
+ export interface m0_ArchiveItem {
2
+ identifier: string;
3
+ title?: string;
4
+ mediatype?: string;
5
+ year?: string;
6
+ downloads?: number;
7
+ collection?: string[];
8
+ description?: string;
9
+ }
10
+ export interface m0_ArchiveFile {
11
+ name: string;
12
+ size?: number;
13
+ format?: string;
14
+ length?: string;
15
+ title?: string;
16
+ }
17
+ export interface m1_Snapshot {
18
+ timestamp: string;
19
+ original: string;
20
+ statusCode: string;
21
+ mimeType: string;
22
+ length: string;
23
+ url: string;
24
+ }
25
+ export declare const ArchiveError: {
26
+ new (message?: string): {
27
+ name: string;
28
+ message: string;
29
+ stack?: string;
30
+ cause?: unknown;
31
+ };
32
+ new (message?: string, options?: ErrorOptions): {
33
+ name: string;
34
+ message: string;
35
+ stack?: string;
36
+ cause?: unknown;
37
+ };
38
+ captureStackTrace(targetObject: object, constructorOpt?: Function): void;
39
+ prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]): any;
40
+ stackTraceLimit: number;
41
+ };
42
+ export declare const WaybackError: {
43
+ new (message?: string): {
44
+ name: string;
45
+ message: string;
46
+ stack?: string;
47
+ cause?: unknown;
48
+ };
49
+ new (message?: string, options?: ErrorOptions): {
50
+ name: string;
51
+ message: string;
52
+ stack?: string;
53
+ cause?: unknown;
54
+ };
55
+ captureStackTrace(targetObject: object, constructorOpt?: Function): void;
56
+ prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]): any;
57
+ stackTraceLimit: number;
58
+ };
59
+ export declare const fmtSize: (bytes?: number) => string;
60
+ export declare const fmtTimestamp: (ts: string) => string;
61
+ export declare const formatItem: (it: m0_ArchiveItem, index: number) => string;
62
+ export declare const formatSnapshot: (s: m1_Snapshot, index?: number) => string;
63
+ export declare const getAvailability: (url: string) => Promise<m1_Snapshot | null>;
64
+ export declare const getItemDetails: (identifier: string) => Promise<{
65
+ identifier: string;
66
+ title?: string;
67
+ mediatype?: string;
68
+ description?: string;
69
+ files: m0_ArchiveFile[];
70
+ }>;
71
+ export declare const getSnapshotText: (timestamp: string, originalUrl: string, maxChars?: number) => Promise<string>;
72
+ export declare const getSnapshots: (url: string, limit?: number) => Promise<m1_Snapshot[]>;
73
+ export declare const searchItems: (query: string, mediatype: string | undefined, limit?: number) => Promise<m0_ArchiveItem[]>;
74
+ export declare const m0_fmtSize: (bytes?: number) => string;
75
+ export declare const m0_ArchiveError: {
76
+ new (message?: string): {
77
+ name: string;
78
+ message: string;
79
+ stack?: string;
80
+ cause?: unknown;
81
+ };
82
+ new (message?: string, options?: ErrorOptions): {
83
+ name: string;
84
+ message: string;
85
+ stack?: string;
86
+ cause?: unknown;
87
+ };
88
+ captureStackTrace(targetObject: object, constructorOpt?: Function): void;
89
+ prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]): any;
90
+ stackTraceLimit: number;
91
+ };
92
+ export declare const m0_getItemDetails: (identifier: string) => Promise<{
93
+ identifier: string;
94
+ title?: string;
95
+ mediatype?: string;
96
+ description?: string;
97
+ files: m0_ArchiveFile[];
98
+ }>;
99
+ export declare const m0_formatItem: (it: m0_ArchiveItem, index: number) => string;
100
+ export declare const m0_searchItems: (query: string, mediatype: string | undefined, limit?: number) => Promise<m0_ArchiveItem[]>;
101
+ export declare const m1_fmtTimestamp: (ts: string) => string;
102
+ export declare const m1_formatSnapshot: (s: m1_Snapshot, index?: number) => string;
103
+ export declare const m1_getAvailability: (url: string) => Promise<m1_Snapshot | null>;
104
+ export declare const m1_WaybackError: {
105
+ new (message?: string): {
106
+ name: string;
107
+ message: string;
108
+ stack?: string;
109
+ cause?: unknown;
110
+ };
111
+ new (message?: string, options?: ErrorOptions): {
112
+ name: string;
113
+ message: string;
114
+ stack?: string;
115
+ cause?: unknown;
116
+ };
117
+ captureStackTrace(targetObject: object, constructorOpt?: Function): void;
118
+ prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]): any;
119
+ stackTraceLimit: number;
120
+ };
121
+ export declare const m1_getSnapshotText: (timestamp: string, originalUrl: string, maxChars?: number) => Promise<string>;
122
+ export declare const m1_getSnapshots: (url: string, limit?: number) => Promise<m1_Snapshot[]>;
package/dist/api.js ADDED
@@ -0,0 +1,183 @@
1
+ const m0 = (() => {
2
+ /**
3
+ * Internet Archive client — keyless.
4
+ * - advancedsearch.php: full-text + metadata search over ~200M items.
5
+ * - metadata/{identifier}: file manifests for an item.
6
+ */
7
+ const SEARCH = "https://archive.org/advancedsearch.php";
8
+ const META = "https://archive.org/metadata";
9
+ class ArchiveError extends Error {
10
+ }
11
+ async function getJson(url) {
12
+ const res = await fetch(url, { headers: { "User-Agent": "archive-org-mcp/1.0" } });
13
+ if (!res.ok)
14
+ throw new ArchiveError(`Internet Archive error ${res.status} for ${url.slice(0, 120)}`);
15
+ return (await res.json());
16
+ }
17
+ async function searchItems(query, mediatype, limit = 8) {
18
+ const q = mediatype ? `(${query}) AND mediatype:${mediatype}` : query;
19
+ const fl = ["identifier", "title", "mediatype", "year", "downloads", "collection", "description"].join(",");
20
+ const url = `${SEARCH}?q=${encodeURIComponent(q)}&fl[]=${fl}&rows=${limit}&page=1&output=json`;
21
+ const data = await getJson(url);
22
+ return (data.response?.docs ?? []).map((d) => ({
23
+ identifier: d.identifier ?? "",
24
+ title: d.title ?? d.identifier,
25
+ mediatype: d.mediatype,
26
+ year: d.year,
27
+ downloads: d.downloads,
28
+ collection: d.collection,
29
+ description: d.description,
30
+ }));
31
+ }
32
+ async function getItemDetails(identifier) {
33
+ const data = await getJson(`${META}/${encodeURIComponent(identifier)}`);
34
+ const files = (data.files ?? [])
35
+ .filter((f) => f.name && !f.name.endsWith("_files.xml") && !f.name.endsWith("_meta.xml"))
36
+ .map((f) => ({
37
+ name: f.name ?? "",
38
+ size: f.size,
39
+ format: f.format,
40
+ length: f.length,
41
+ title: f.title,
42
+ }))
43
+ .slice(0, 30);
44
+ return {
45
+ identifier: data.metadata?.identifier ?? identifier,
46
+ title: data.metadata?.title ?? data.metadata?.identifier,
47
+ mediatype: data.metadata?.mediatype,
48
+ description: data.metadata?.description,
49
+ files,
50
+ };
51
+ }
52
+ function fmtSize(bytes) {
53
+ if (!bytes)
54
+ return "";
55
+ const units = ["B", "KB", "MB", "GB", "TB"];
56
+ let v = bytes;
57
+ let i = 0;
58
+ while (v >= 1024 && i < units.length - 1) {
59
+ v /= 1024;
60
+ i++;
61
+ }
62
+ return `${v.toFixed(1)} ${units[i]}`;
63
+ }
64
+ function formatItem(it, index) {
65
+ const meta = [
66
+ it.mediatype,
67
+ it.year,
68
+ it.downloads ? `${it.downloads.toLocaleString()} downloads` : "",
69
+ ]
70
+ .filter(Boolean)
71
+ .join(" · ");
72
+ const lines = [
73
+ `${index + 1}. ${it.title ?? it.identifier} [${it.identifier}]`,
74
+ meta,
75
+ `https://archive.org/details/${it.identifier}`,
76
+ ].filter(Boolean);
77
+ return lines.join("\n");
78
+ }
79
+ return { ArchiveError, fmtSize, formatItem, getItemDetails, searchItems };
80
+ })();
81
+ const m1 = (() => {
82
+ /**
83
+ * Wayback Machine client. Keyless.
84
+ * - CDX API lists snapshot history for a URL.
85
+ * - /wayback/available finds the nearest snapshot.
86
+ * - /web/{timestamp}id_/{url} serves the archived page content.
87
+ */
88
+ const CDX = "https://web.archive.org/cdx/search/cdx";
89
+ const AVAILABLE = "https://archive.org/wayback/available";
90
+ class WaybackError extends Error {
91
+ }
92
+ async function getText(url) {
93
+ const res = await fetch(url, {
94
+ headers: { "User-Agent": "wayback-mcp/1.0", Accept: "application/json,text/plain,text/html" },
95
+ });
96
+ if (!res.ok)
97
+ throw new WaybackError(`Wayback error ${res.status} for ${url.slice(0, 100)}`);
98
+ return res.text();
99
+ }
100
+ async function getSnapshots(url, limit = 10) {
101
+ const q = `${CDX}?url=${encodeURIComponent(url)}&output=json&fl=timestamp,original,statuscode,mimetype,length&filter=statuscode:200&limit=${limit}`;
102
+ const text = await getText(q);
103
+ const rows = JSON.parse(text);
104
+ if (rows.length <= 1)
105
+ return [];
106
+ const [, ...data] = rows;
107
+ return data.map((r) => ({
108
+ timestamp: r[0] ?? "",
109
+ original: r[1] ?? url,
110
+ statusCode: r[2] ?? "",
111
+ mimeType: r[3] ?? "",
112
+ length: r[4] ?? "",
113
+ url: `https://web.archive.org/web/${r[0]}id_/${r[1] ?? url}`,
114
+ }));
115
+ }
116
+ async function getAvailability(url) {
117
+ const text = await getText(`${AVAILABLE}?url=${encodeURIComponent(url)}`);
118
+ const d = JSON.parse(text);
119
+ const closest = d.archived_snapshots?.closest;
120
+ if (!closest?.url)
121
+ return null;
122
+ return {
123
+ timestamp: closest.timestamp ?? "",
124
+ original: url,
125
+ statusCode: closest.status ?? "",
126
+ mimeType: "",
127
+ length: "",
128
+ url: closest.url,
129
+ };
130
+ }
131
+ async function getSnapshotText(timestamp, originalUrl, maxChars = 15000) {
132
+ const target = `https://web.archive.org/web/${timestamp}id_/${originalUrl}`;
133
+ const raw = await getText(target);
134
+ const plain = raw
135
+ .replace(/<script[\s\S]*?<\/script>/gi, " ")
136
+ .replace(/<style[\s\S]*?<\/style>/gi, " ")
137
+ .replace(/<[^>]+>/g, " ")
138
+ .replace(/&nbsp;/g, " ")
139
+ .replace(/&amp;/g, "&")
140
+ .replace(/&lt;/g, "<")
141
+ .replace(/&gt;/g, ">")
142
+ .replace(/\s+/g, " ")
143
+ .trim();
144
+ return plain.slice(0, maxChars);
145
+ }
146
+ function fmtTimestamp(ts) {
147
+ if (ts.length < 12)
148
+ return ts;
149
+ return `${ts.slice(0, 4)}-${ts.slice(4, 6)}-${ts.slice(6, 8)} ${ts.slice(8, 10)}:${ts.slice(10, 12)} UTC`;
150
+ }
151
+ function formatSnapshot(s, index) {
152
+ const head = `${index !== undefined ? `${index + 1}. ` : ""}${fmtTimestamp(s.timestamp)}`;
153
+ const lines = [
154
+ head,
155
+ `Status ${s.statusCode || "?"}${s.mimeType ? ` · ${s.mimeType}` : ""}${s.length ? ` · ${s.length} bytes` : ""}`,
156
+ s.url,
157
+ ].filter(Boolean);
158
+ return lines.join("\n");
159
+ }
160
+ return { WaybackError, fmtTimestamp, formatSnapshot, getAvailability, getSnapshotText, getSnapshots };
161
+ })();
162
+ export const ArchiveError = m0.ArchiveError;
163
+ export const WaybackError = m1.WaybackError;
164
+ export const fmtSize = m0.fmtSize;
165
+ export const fmtTimestamp = m1.fmtTimestamp;
166
+ export const formatItem = m0.formatItem;
167
+ export const formatSnapshot = m1.formatSnapshot;
168
+ export const getAvailability = m1.getAvailability;
169
+ export const getItemDetails = m0.getItemDetails;
170
+ export const getSnapshotText = m1.getSnapshotText;
171
+ export const getSnapshots = m1.getSnapshots;
172
+ export const searchItems = m0.searchItems;
173
+ export const m0_fmtSize = m0.fmtSize;
174
+ export const m0_ArchiveError = m0.ArchiveError;
175
+ export const m0_getItemDetails = m0.getItemDetails;
176
+ export const m0_formatItem = m0.formatItem;
177
+ export const m0_searchItems = m0.searchItems;
178
+ export const m1_fmtTimestamp = m1.fmtTimestamp;
179
+ export const m1_formatSnapshot = m1.formatSnapshot;
180
+ export const m1_getAvailability = m1.getAvailability;
181
+ export const m1_WaybackError = m1.WaybackError;
182
+ export const m1_getSnapshotText = m1.getSnapshotText;
183
+ export const m1_getSnapshots = m1.getSnapshots;
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ import { createServer } from "./server.js";
3
+ async function main() {
4
+ const server = createServer();
5
+ const transport = new StdioServerTransport();
6
+ await server.connect(transport);
7
+ console.error("MCP server running on stdio");
8
+ }
9
+ main().catch((err) => {
10
+ console.error("Fatal error:", err);
11
+ process.exit(1);
12
+ });
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function createServer(): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,112 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { m0_fmtSize, m0_formatItem, m0_getItemDetails, m0_searchItems, m1_formatSnapshot, m1_getAvailability, m1_getSnapshots, m1_getSnapshotText } from './api.js';
4
+ const text = (value) => ({ content: [{ type: 'text', text: value }] });
5
+ const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
6
+ const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
7
+ const error = (e) => `Error: ${e instanceof Error ? e.message : String(e)}`;
8
+ const errorMessage = error;
9
+ export function createServer() {
10
+ const server = new McpServer({ name: 'archive-org-mcp', version: '1.0.0' });
11
+ server.registerTool("search_items", {
12
+ title: "Search items",
13
+ description: "Search the Internet Archive — old software, abandoned CD-ROM games, " +
14
+ "bootleg concert tapes, 78rpm records, dead web pages, and more.",
15
+ inputSchema: z.object({
16
+ query: z.string().describe("Search query, e.g. 'Windows 95' or 'sonic hedgehog prototype'"),
17
+ mediatype: z
18
+ .enum(["software", "audio", "movies", "texts", "image", "web"])
19
+ .optional()
20
+ .describe("Restrict to a media type"),
21
+ limit: z.number().int().min(1).max(25).default(8),
22
+ }),
23
+ annotations: READ_ONLY,
24
+ }, async ({ query, mediatype, limit }) => {
25
+ try {
26
+ const items = await m0_searchItems(query, mediatype, limit);
27
+ if (items.length === 0)
28
+ return text(`Nothing in the Archive matches "${query}".`);
29
+ return text(`Archive.org results for "${query}"${mediatype ? ` (${mediatype})` : ""}:\n\n` +
30
+ items.map((it, i) => m0_formatItem(it, i)).join("\n\n"));
31
+ }
32
+ catch (e) {
33
+ return textError(errorMessage(e));
34
+ }
35
+ });
36
+ server.registerTool("get_item", {
37
+ title: "Get item",
38
+ description: "Get full details + file manifest for an archive.org item.",
39
+ inputSchema: z.object({ identifier: z.string().describe("Item identifier from search_items") }),
40
+ annotations: READ_ONLY,
41
+ }, async ({ identifier }) => {
42
+ try {
43
+ const d = await m0_getItemDetails(identifier);
44
+ const lines = [
45
+ d.title ?? d.identifier,
46
+ d.mediatype ? `Type: ${d.mediatype}` : "",
47
+ d.description ? d.description.slice(0, 400) : "",
48
+ `https://archive.org/details/${d.identifier}`,
49
+ "",
50
+ `Files (${d.files.length} shown):`,
51
+ ...d.files.map((f, i) => `${i + 1}. ${f.name}${f.format ? ` [${f.format}]` : ""}${f.size ? ` ${m0_fmtSize(f.size)}` : ""}${f.length ? ` (${f.length})` : ""}`),
52
+ ].filter(Boolean);
53
+ return text(lines.join("\n"));
54
+ }
55
+ catch (e) {
56
+ return textError(errorMessage(e));
57
+ }
58
+ });
59
+ server.registerTool("get_snapshots", {
60
+ title: "Get snapshots",
61
+ description: "List the Wayback Machine snapshot history for a URL.",
62
+ inputSchema: z.object({ url: z.string().describe("Full URL, e.g. 'example.com/page.html'"), limit: z.number().int().min(1).max(25).default(10) }),
63
+ annotations: READ_ONLY,
64
+ }, async ({ url, limit }) => {
65
+ try {
66
+ const snaps = await m1_getSnapshots(url, limit);
67
+ if (snaps.length === 0)
68
+ return text(`No archived snapshots of ${url}.`);
69
+ return text(`Snapshots of ${url}:\n\n${snaps.map((s, i) => m1_formatSnapshot(s, i)).join("\n\n")}`);
70
+ }
71
+ catch (e) {
72
+ return textError(errorMessage(e));
73
+ }
74
+ });
75
+ server.registerTool("get_availability", {
76
+ title: "Get availability",
77
+ description: "Check whether a page is archived and get its closest snapshot.",
78
+ inputSchema: z.object({ url: z.string().describe("Full URL") }),
79
+ annotations: READ_ONLY,
80
+ }, async ({ url }) => {
81
+ try {
82
+ const snap = await m1_getAvailability(url);
83
+ if (!snap)
84
+ return text(`No archived snapshot of ${url}.`);
85
+ return text(`Closest snapshot of ${url}:\n\n${m1_formatSnapshot(snap)}`);
86
+ }
87
+ catch (e) {
88
+ return textError(errorMessage(e));
89
+ }
90
+ });
91
+ server.registerTool("read_snapshot", {
92
+ title: "Read snapshot",
93
+ description: "Read the text content of an archived page, for reading dead sites.",
94
+ inputSchema: z.object({
95
+ timestamp: z.string().describe("Snapshot timestamp from get_snapshots, e.g. '20240101120000'"),
96
+ url: z.string().describe("The original URL that was archived"),
97
+ maxChars: z.number().int().min(500).max(50000).default(15000),
98
+ }),
99
+ annotations: READ_ONLY,
100
+ }, async ({ timestamp, url, maxChars }) => {
101
+ try {
102
+ const content = await m1_getSnapshotText(timestamp, url, maxChars);
103
+ if (!content.trim())
104
+ return text(`Snapshot ${timestamp} of ${url} has no readable text.`);
105
+ return text(content);
106
+ }
107
+ catch (e) {
108
+ return textError(errorMessage(e));
109
+ }
110
+ });
111
+ return server;
112
+ }
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "archive-org-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Internet Archive search, item metadata, and Wayback snapshots.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "scripts": {
8
+ "build": "tsc -p tsconfig.json",
9
+ "start": "node dist/index.js"
10
+ },
11
+ "dependencies": {
12
+ "@modelcontextprotocol/sdk": "^1.0.0",
13
+ "zod": "^3.23.0"
14
+ },
15
+ "devDependencies": {
16
+ "typescript": "^5.5.0",
17
+ "@types/node": "^20.0.0"
18
+ },
19
+ "license": "MIT",
20
+ "engines": {
21
+ "node": ">=20"
22
+ },
23
+ "mcpName": "io.github.mrfentmen/archive-org-mcp"
24
+ }
package/server.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.mrfentmen/archive-org-mcp",
4
+ "description": "Internet Archive search, item metadata, and Wayback snapshots.",
5
+ "repository": {
6
+ "url": "https://github.com/mrfentmen/awesome-mcps",
7
+ "source": "github"
8
+ },
9
+ "version": "1.0.0",
10
+ "packages": [
11
+ {
12
+ "registryType": "npm",
13
+ "identifier": "archive-org-mcp",
14
+ "version": "1.0.0",
15
+ "transport": {
16
+ "type": "stdio"
17
+ }
18
+ }
19
+ ]
20
+ }
package/src/api.ts ADDED
@@ -0,0 +1,237 @@
1
+
2
+ export interface m0_ArchiveItem {
3
+ identifier: string
4
+ title?: string
5
+ mediatype?: string
6
+ year?: string
7
+ downloads?: number
8
+ collection?: string[]
9
+ description?: string
10
+ }
11
+
12
+ export interface m0_ArchiveFile {
13
+ name: string
14
+ size?: number
15
+ format?: string
16
+ length?: string
17
+ title?: string
18
+ }
19
+
20
+ export interface m1_Snapshot {
21
+ timestamp: string
22
+ original: string
23
+ statusCode: string
24
+ mimeType: string
25
+ length: string
26
+ url: string
27
+ }
28
+
29
+ const m0 = (() => {
30
+ /**
31
+ * Internet Archive client — keyless.
32
+ * - advancedsearch.php: full-text + metadata search over ~200M items.
33
+ * - metadata/{identifier}: file manifests for an item.
34
+ */
35
+ const SEARCH = "https://archive.org/advancedsearch.php"
36
+ const META = "https://archive.org/metadata"
37
+
38
+ class ArchiveError extends Error {}
39
+
40
+
41
+
42
+ async function getJson<T>(url: string): Promise<T> {
43
+ const res = await fetch(url, { headers: { "User-Agent": "archive-org-mcp/1.0" } })
44
+ if (!res.ok) throw new ArchiveError(`Internet Archive error ${res.status} for ${url.slice(0, 120)}`)
45
+ return (await res.json()) as T
46
+ }
47
+
48
+ async function searchItems(
49
+ query: string,
50
+ mediatype: string | undefined,
51
+ limit = 8
52
+ ): Promise<m0_ArchiveItem[]> {
53
+ const q = mediatype ? `(${query}) AND mediatype:${mediatype}` : query
54
+ const fl = ["identifier", "title", "mediatype", "year", "downloads", "collection", "description"].join(",")
55
+ const url = `${SEARCH}?q=${encodeURIComponent(q)}&fl[]=${fl}&rows=${limit}&page=1&output=json`
56
+ const data = await getJson<{ response?: { docs?: any[] } }>(url)
57
+ return (data.response?.docs ?? []).map((d) => ({
58
+ identifier: d.identifier ?? "",
59
+ title: d.title ?? d.identifier,
60
+ mediatype: d.mediatype,
61
+ year: d.year,
62
+ downloads: d.downloads,
63
+ collection: d.collection,
64
+ description: d.description,
65
+ }))
66
+ }
67
+
68
+ async function getItemDetails(identifier: string): Promise<{
69
+ identifier: string
70
+ title?: string
71
+ mediatype?: string
72
+ description?: string
73
+ files: m0_ArchiveFile[]
74
+ }> {
75
+ const data = await getJson<any>(`${META}/${encodeURIComponent(identifier)}`)
76
+ const files: m0_ArchiveFile[] = (data.files ?? [])
77
+ .filter((f: any) => f.name && !f.name.endsWith("_files.xml") && !f.name.endsWith("_meta.xml"))
78
+ .map((f: any) => ({
79
+ name: f.name ?? "",
80
+ size: f.size,
81
+ format: f.format,
82
+ length: f.length,
83
+ title: f.title,
84
+ }))
85
+ .slice(0, 30)
86
+ return {
87
+ identifier: data.metadata?.identifier ?? identifier,
88
+ title: data.metadata?.title ?? data.metadata?.identifier,
89
+ mediatype: data.metadata?.mediatype,
90
+ description: data.metadata?.description,
91
+ files,
92
+ }
93
+ }
94
+
95
+ function fmtSize(bytes?: number): string {
96
+ if (!bytes) return ""
97
+ const units = ["B", "KB", "MB", "GB", "TB"]
98
+ let v = bytes
99
+ let i = 0
100
+ while (v >= 1024 && i < units.length - 1) {
101
+ v /= 1024
102
+ i++
103
+ }
104
+ return `${v.toFixed(1)} ${units[i]}`
105
+ }
106
+
107
+ function formatItem(it: m0_ArchiveItem, index: number): string {
108
+ const meta = [
109
+ it.mediatype,
110
+ it.year,
111
+ it.downloads ? `${it.downloads.toLocaleString()} downloads` : "",
112
+ ]
113
+ .filter(Boolean)
114
+ .join(" · ")
115
+ const lines = [
116
+ `${index + 1}. ${it.title ?? it.identifier} [${it.identifier}]`,
117
+ meta,
118
+ `https://archive.org/details/${it.identifier}`,
119
+ ].filter(Boolean)
120
+ return lines.join("\n")
121
+ }
122
+
123
+ return { ArchiveError, fmtSize, formatItem, getItemDetails, searchItems };
124
+ })();
125
+
126
+ const m1 = (() => {
127
+ /**
128
+ * Wayback Machine client. Keyless.
129
+ * - CDX API lists snapshot history for a URL.
130
+ * - /wayback/available finds the nearest snapshot.
131
+ * - /web/{timestamp}id_/{url} serves the archived page content.
132
+ */
133
+ const CDX = "https://web.archive.org/cdx/search/cdx"
134
+ const AVAILABLE = "https://archive.org/wayback/available"
135
+
136
+ class WaybackError extends Error {}
137
+
138
+
139
+ async function getText(url: string): Promise<string> {
140
+ const res = await fetch(url, {
141
+ headers: { "User-Agent": "wayback-mcp/1.0", Accept: "application/json,text/plain,text/html" },
142
+ })
143
+ if (!res.ok) throw new WaybackError(`Wayback error ${res.status} for ${url.slice(0, 100)}`)
144
+ return res.text()
145
+ }
146
+
147
+ async function getSnapshots(url: string, limit = 10): Promise<m1_Snapshot[]> {
148
+ const q = `${CDX}?url=${encodeURIComponent(url)}&output=json&fl=timestamp,original,statuscode,mimetype,length&filter=statuscode:200&limit=${limit}`
149
+ const text = await getText(q)
150
+ const rows: string[][] = JSON.parse(text)
151
+ if (rows.length <= 1) return []
152
+ const [, ...data] = rows
153
+ return data.map((r) => ({
154
+ timestamp: r[0] ?? "",
155
+ original: r[1] ?? url,
156
+ statusCode: r[2] ?? "",
157
+ mimeType: r[3] ?? "",
158
+ length: r[4] ?? "",
159
+ url: `https://web.archive.org/web/${r[0]}id_/${r[1] ?? url}`,
160
+ }))
161
+ }
162
+
163
+ async function getAvailability(url: string): Promise<m1_Snapshot | null> {
164
+ const text = await getText(`${AVAILABLE}?url=${encodeURIComponent(url)}`)
165
+ const d = JSON.parse(text) as { archived_snapshots?: { closest?: { url?: string; timestamp?: string; status?: string } } }
166
+ const closest = d.archived_snapshots?.closest
167
+ if (!closest?.url) return null
168
+ return {
169
+ timestamp: closest.timestamp ?? "",
170
+ original: url,
171
+ statusCode: closest.status ?? "",
172
+ mimeType: "",
173
+ length: "",
174
+ url: closest.url,
175
+ }
176
+ }
177
+
178
+ async function getSnapshotText(
179
+ timestamp: string,
180
+ originalUrl: string,
181
+ maxChars = 15000
182
+ ): Promise<string> {
183
+ const target = `https://web.archive.org/web/${timestamp}id_/${originalUrl}`
184
+ const raw = await getText(target)
185
+ const plain = raw
186
+ .replace(/<script[\s\S]*?<\/script>/gi, " ")
187
+ .replace(/<style[\s\S]*?<\/style>/gi, " ")
188
+ .replace(/<[^>]+>/g, " ")
189
+ .replace(/&nbsp;/g, " ")
190
+ .replace(/&amp;/g, "&")
191
+ .replace(/&lt;/g, "<")
192
+ .replace(/&gt;/g, ">")
193
+ .replace(/\s+/g, " ")
194
+ .trim()
195
+ return plain.slice(0, maxChars)
196
+ }
197
+
198
+ function fmtTimestamp(ts: string): string {
199
+ if (ts.length < 12) return ts
200
+ return `${ts.slice(0, 4)}-${ts.slice(4, 6)}-${ts.slice(6, 8)} ${ts.slice(8, 10)}:${ts.slice(10, 12)} UTC`
201
+ }
202
+
203
+ function formatSnapshot(s: m1_Snapshot, index?: number): string {
204
+ const head = `${index !== undefined ? `${index + 1}. ` : ""}${fmtTimestamp(s.timestamp)}`
205
+ const lines = [
206
+ head,
207
+ `Status ${s.statusCode || "?"}${s.mimeType ? ` · ${s.mimeType}` : ""}${s.length ? ` · ${s.length} bytes` : ""}`,
208
+ s.url,
209
+ ].filter(Boolean)
210
+ return lines.join("\n")
211
+ }
212
+
213
+ return { WaybackError, fmtTimestamp, formatSnapshot, getAvailability, getSnapshotText, getSnapshots };
214
+ })();
215
+
216
+ export const ArchiveError = m0.ArchiveError;
217
+ export const WaybackError = m1.WaybackError;
218
+ export const fmtSize = m0.fmtSize;
219
+ export const fmtTimestamp = m1.fmtTimestamp;
220
+ export const formatItem = m0.formatItem;
221
+ export const formatSnapshot = m1.formatSnapshot;
222
+ export const getAvailability = m1.getAvailability;
223
+ export const getItemDetails = m0.getItemDetails;
224
+ export const getSnapshotText = m1.getSnapshotText;
225
+ export const getSnapshots = m1.getSnapshots;
226
+ export const searchItems = m0.searchItems;
227
+ export const m0_fmtSize = m0.fmtSize;
228
+ export const m0_ArchiveError = m0.ArchiveError;
229
+ export const m0_getItemDetails = m0.getItemDetails;
230
+ export const m0_formatItem = m0.formatItem;
231
+ export const m0_searchItems = m0.searchItems;
232
+ export const m1_fmtTimestamp = m1.fmtTimestamp;
233
+ export const m1_formatSnapshot = m1.formatSnapshot;
234
+ export const m1_getAvailability = m1.getAvailability;
235
+ export const m1_WaybackError = m1.WaybackError;
236
+ export const m1_getSnapshotText = m1.getSnapshotText;
237
+ export const m1_getSnapshots = m1.getSnapshots;
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
2
+ import { createServer } from "./server.js"
3
+
4
+ async function main(): Promise<void> {
5
+ const server = createServer()
6
+ const transport = new StdioServerTransport()
7
+ await server.connect(transport)
8
+ console.error("MCP server running on stdio")
9
+ }
10
+
11
+ main().catch((err: unknown) => {
12
+ console.error("Fatal error:", err)
13
+ process.exit(1)
14
+ })
package/src/server.ts ADDED
@@ -0,0 +1,135 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
+ import { z } from 'zod'
3
+ import { m0_ArchiveError, m0_fmtSize, m0_formatItem, m0_getItemDetails, m0_searchItems, m1_WaybackError, m1_formatSnapshot, m1_getAvailability, m1_getSnapshots, m1_getSnapshotText } from './api.js'
4
+
5
+ const text = (value: string) => ({ content: [{ type: 'text' as const, text: value }] })
6
+ const textError = (t: string) => ({ content: [{ type: "text" as const, text: t }], isError: true as const })
7
+ const READ_ONLY = { readOnlyHint: true, openWorldHint: true } as const
8
+ const error = (e: unknown) => `Error: ${e instanceof Error ? e.message : String(e)}`
9
+ const errorMessage = error
10
+
11
+ export function createServer(): McpServer {
12
+ const server = new McpServer({ name: 'archive-org-mcp', version: '1.0.0' })
13
+ server.registerTool(
14
+ "search_items",
15
+ {
16
+ title: "Search items",
17
+ description: "Search the Internet Archive — old software, abandoned CD-ROM games, " +
18
+ "bootleg concert tapes, 78rpm records, dead web pages, and more.",
19
+ inputSchema: z.object(
20
+ {
21
+ query: z.string().describe("Search query, e.g. 'Windows 95' or 'sonic hedgehog prototype'"),
22
+ mediatype: z
23
+ .enum(["software", "audio", "movies", "texts", "image", "web"])
24
+ .optional()
25
+ .describe("Restrict to a media type"),
26
+ limit: z.number().int().min(1).max(25).default(8),
27
+ }),
28
+ annotations: READ_ONLY,
29
+ },
30
+ async ({ query, mediatype, limit }) => {
31
+ try {
32
+ const items = await m0_searchItems(query, mediatype, limit)
33
+ if (items.length === 0) return text(`Nothing in the Archive matches "${query}".`)
34
+ return text(
35
+ `Archive.org results for "${query}"${mediatype ? ` (${mediatype})` : ""}:\n\n` +
36
+ items.map((it, i) => m0_formatItem(it, i)).join("\n\n")
37
+ )
38
+ } catch (e) {
39
+ return textError(errorMessage(e))
40
+ }
41
+ }
42
+ )
43
+ server.registerTool(
44
+ "get_item",
45
+ {
46
+ title: "Get item",
47
+ description: "Get full details + file manifest for an archive.org item.",
48
+ inputSchema: z.object(
49
+ { identifier: z.string().describe("Item identifier from search_items") }),
50
+ annotations: READ_ONLY,
51
+ },
52
+ async ({ identifier }) => {
53
+ try {
54
+ const d = await m0_getItemDetails(identifier)
55
+ const lines = [
56
+ d.title ?? d.identifier,
57
+ d.mediatype ? `Type: ${d.mediatype}` : "",
58
+ d.description ? d.description.slice(0, 400) : "",
59
+ `https://archive.org/details/${d.identifier}`,
60
+ "",
61
+ `Files (${d.files.length} shown):`,
62
+ ...d.files.map(
63
+ (f, i) =>
64
+ `${i + 1}. ${f.name}${f.format ? ` [${f.format}]` : ""}${f.size ? ` ${m0_fmtSize(f.size)}` : ""}${f.length ? ` (${f.length})` : ""}`
65
+ ),
66
+ ].filter(Boolean)
67
+ return text(lines.join("\n"))
68
+ } catch (e) {
69
+ return textError(errorMessage(e))
70
+ }
71
+ }
72
+ )
73
+ server.registerTool(
74
+ "get_snapshots",
75
+ {
76
+ title: "Get snapshots",
77
+ description: "List the Wayback Machine snapshot history for a URL.",
78
+ inputSchema: z.object(
79
+ { url: z.string().describe("Full URL, e.g. 'example.com/page.html'"), limit: z.number().int().min(1).max(25).default(10) }),
80
+ annotations: READ_ONLY,
81
+ },
82
+ async ({ url, limit }) => {
83
+ try {
84
+ const snaps = await m1_getSnapshots(url, limit)
85
+ if (snaps.length === 0) return text(`No archived snapshots of ${url}.`)
86
+ return text(`Snapshots of ${url}:\n\n${snaps.map((s, i) => m1_formatSnapshot(s, i)).join("\n\n")}`)
87
+ } catch (e) {
88
+ return textError(errorMessage(e))
89
+ }
90
+ }
91
+ )
92
+ server.registerTool(
93
+ "get_availability",
94
+ {
95
+ title: "Get availability",
96
+ description: "Check whether a page is archived and get its closest snapshot.",
97
+ inputSchema: z.object(
98
+ { url: z.string().describe("Full URL") }),
99
+ annotations: READ_ONLY,
100
+ },
101
+ async ({ url }) => {
102
+ try {
103
+ const snap = await m1_getAvailability(url)
104
+ if (!snap) return text(`No archived snapshot of ${url}.`)
105
+ return text(`Closest snapshot of ${url}:\n\n${m1_formatSnapshot(snap)}`)
106
+ } catch (e) {
107
+ return textError(errorMessage(e))
108
+ }
109
+ }
110
+ )
111
+ server.registerTool(
112
+ "read_snapshot",
113
+ {
114
+ title: "Read snapshot",
115
+ description: "Read the text content of an archived page, for reading dead sites.",
116
+ inputSchema: z.object(
117
+ {
118
+ timestamp: z.string().describe("Snapshot timestamp from get_snapshots, e.g. '20240101120000'"),
119
+ url: z.string().describe("The original URL that was archived"),
120
+ maxChars: z.number().int().min(500).max(50000).default(15000),
121
+ }),
122
+ annotations: READ_ONLY,
123
+ },
124
+ async ({ timestamp, url, maxChars }) => {
125
+ try {
126
+ const content = await m1_getSnapshotText(timestamp, url, maxChars)
127
+ if (!content.trim()) return text(`Snapshot ${timestamp} of ${url} has no readable text.`)
128
+ return text(content)
129
+ } catch (e) {
130
+ return textError(errorMessage(e))
131
+ }
132
+ }
133
+ )
134
+ return server
135
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "declaration": true,
13
+ "resolveJsonModule": true
14
+ },
15
+ "include": ["src/**/*"]
16
+ }