mcp-wayback-machine
Version:
MCP server and CLI tool for interacting with the Wayback Machine without API keys
98 lines • 4.26 kB
JavaScript
/**
* Compare tool ΓÇö diffs two archived snapshots using Wayback Changes.
* Fetches raw content of both snapshots and provides a visual diff URL.
*/
import * as z from "zod";
import { CdxResponse } from "../schemas.js";
import { HttpError } from "../utils/http.js";
import { capContent, formatTimestamp, HttpUrl, Timestamp, } from "../utils/validation.js";
export const CompareSnapshots = z.object({
url: HttpUrl.meta({ description: "The URL to compare snapshots for" }),
timestampA: Timestamp.optional().meta({
description: "First timestamp (YYYYMMDDhhmmss). Defaults to oldest available.",
}),
timestampB: Timestamp.optional().meta({
description: "Second timestamp (YYYYMMDDhhmmss). Defaults to newest available.",
}),
});
/**
* Compare two archived snapshots of a URL.
* Fetches the snapshot content for both timestamps and returns them
* for comparison. Also provides the Wayback Changes visual diff URL.
*/
export async function compareSnapshots(input, ctx) {
const { url, timestampA, timestampB } = input;
try {
// If both timestamps provided, fetch both snapshots directly.
// Both values have been validated against the Timestamp schema
// (14 digits) so they can be safely interpolated into the URL path.
if (timestampA !== undefined && timestampB !== undefined) {
const snapshotAUrl = `https://web.archive.org/web/${timestampA}id_/${url}`;
const snapshotBUrl = `https://web.archive.org/web/${timestampB}id_/${url}`;
const [responseA, responseB] = await Promise.all([
ctx.fetch(snapshotAUrl),
ctx.fetch(snapshotBUrl),
]);
const [contentA, contentB] = await Promise.all([
responseA.text(),
responseB.text(),
]);
return {
success: true,
message: `Comparing snapshots of ${url}`,
snapshotA: {
timestamp: timestampA,
date: formatTimestamp(timestampA),
url: snapshotAUrl,
},
snapshotB: {
timestamp: timestampB,
date: formatTimestamp(timestampB),
url: snapshotBUrl,
},
changesUrl: `https://web.archive.org/web/changes/${encodeURIComponent(url)}`,
contentA: capContent(contentA),
contentB: capContent(contentB),
};
}
// Find available snapshots via CDX API to pick the boundary timestamps
const cdxUrl = new URL("https://web.archive.org/cdx/search/cdx");
cdxUrl.searchParams.set("url", url);
cdxUrl.searchParams.set("output", "json");
cdxUrl.searchParams.set("fl", "timestamp,statuscode");
cdxUrl.searchParams.set("filter", "statuscode:200");
cdxUrl.searchParams.set("limit", "-2");
const data = await ctx.fetchJSON(cdxUrl.toString(), CdxResponse);
// First row is headers, need at least 2 data rows
if (data.length < 3) {
return {
success: false,
message: `Not enough archived snapshots found for ${url} to compare. Need at least 2 snapshots with status 200.`,
};
}
const rows = data.slice(1);
const oldestTs = rows[0]?.[0];
const newestTs = rows[rows.length - 1]?.[0];
if (oldestTs === undefined || newestTs === undefined) {
return {
success: false,
message: `Could not determine snapshot timestamps for ${url}`,
};
}
// Recurse with the resolved timestamps
return await compareSnapshots({ url, timestampA: oldestTs, timestampB: newestTs }, ctx);
}
catch (error) {
if (error instanceof HttpError) {
return {
success: false,
message: `Failed to compare snapshots: ${error.message}`,
};
}
return {
success: false,
message: `Failed to compare snapshots: ${error instanceof Error ? error.message : "Unknown error"}`,
};
}
}
//# sourceMappingURL=compare.js.map