compare-pages
Compare two versions of a wiki page and return a compact text diff. Supports revision IDs, page titles, or wikitext, with an option for cheap change detection.
Instructions
Returns the changes between two versions of a wiki page as a compact text diff. Each side accepts a revision ID, page title (latest revision), or supplied wikitext; text-vs-text is rejected. Only the changes are returned over the wire. For the full text of both sides, fetch with get-page instead. If a title or revision ID does not exist, an error is returned. Set includeDiff=false for a cheap change-detection response that skips diff rendering and returns just the change flag, revision metadata, and size delta. Diff output is truncated at 50000 bytes by default with a trailing marker; a narrower revision range or includeDiff=false avoids truncation.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| fromRevision | No | Revision ID for the "from" side | |
| fromTitle | No | Wiki page title for the "from" side (latest revision is used) | |
| fromText | No | Supplied wikitext for the "from" side | |
| toRevision | No | Revision ID for the "to" side | |
| toTitle | No | Wiki page title for the "to" side (latest revision is used) | |
| toText | No | Supplied wikitext for the "to" side | |
| includeDiff | No | Include the diff body (default true). Set false for a cheap change-detection response. |
Implementation Reference
- src/tools/compare-pages.ts:157-199 (handler)The main tool definition and handler for 'compare-pages'. The `comparePages` object implements the Tool interface with name 'compare-pages', inputSchema (Zod schema for fromRevision/fromTitle/fromText, toRevision/toTitle/toText, includeDiff), and an async handle() function that validates sides, makes an mwn request to the MediaWiki 'compare' API, and builds the response payload.
export const comparePages: Tool<typeof inputSchema> = { name: 'compare-pages', description: 'Returns the changes between two versions of a wiki page as a compact text diff. Each side accepts a revision ID, page title (latest revision), or supplied wikitext; text-vs-text is rejected. Only the changes are returned over the wire. For the full text of both sides, fetch with get-page instead. If a title or revision ID does not exist, an error is returned. Set includeDiff=false for a cheap change-detection response that skips diff rendering and returns just the change flag, revision metadata, and size delta. Diff output is truncated at 50000 bytes by default with a trailing marker; a narrower revision range or includeDiff=false avoids truncation.', inputSchema, annotations: { title: 'Compare pages', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true, } as ToolAnnotations, failureVerb: 'compare pages', async handle(args, ctx: ToolContext): Promise<CallToolResult> { const sideError = validateSide('from', args) ?? validateSide('to', args); if (sideError) { return ctx.format.invalidInput(sideError); } if (args.fromText !== undefined && args.toText !== undefined) { return ctx.format.invalidInput('Cannot compare supplied text against supplied text'); } const includeDiff = args.includeDiff ?? true; const mwn = await ctx.mwn(); const response = await mwn.request({ action: 'compare', prop: includeDiff ? 'ids|title|size|timestamp|diff' : 'ids|title|size|diffsize', formatversion: '2', ...buildSideParams('from', args), ...buildSideParams('to', args), }); // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- mwn API response shape; trusted at this boundary const compare = response.compare as CompareResponse | undefined; if (!compare) { return ctx.format.error( 'upstream_failure', 'Failed to compare pages: no compare result returned', ); } return ctx.format.ok(buildPayload(compare, args, includeDiff)); }, }; - src/tools/compare-pages.ts:23-44 (schema)The inputSchema for compare-pages using Zod validation. Defines seven optional fields: fromRevision, fromTitle, fromText, toRevision, toTitle, toText, and includeDiff. The type ComparePagesArgs is inferred from this schema.
const inputSchema = { fromRevision: z.number().int().positive().optional().describe('Revision ID for the "from" side'), fromTitle: z .string() .optional() .describe('Wiki page title for the "from" side (latest revision is used)'), fromText: z.string().optional().describe('Supplied wikitext for the "from" side'), toRevision: z.number().int().positive().optional().describe('Revision ID for the "to" side'), toTitle: z .string() .optional() .describe('Wiki page title for the "to" side (latest revision is used)'), toText: z.string().optional().describe('Supplied wikitext for the "to" side'), includeDiff: z .boolean() .optional() .describe( 'Include the diff body (default true). Set false for a cheap change-detection response.', ), } as const; type ComparePagesArgs = z.infer<z.ZodObject<typeof inputSchema>>; - src/tools/index.ts:42-50 (registration)comparePages is imported from './compare-pages.js' and added to the standardTools array at line 50. It is then registered with the MCP server via the register() function in registerAllTools().
const standardTools: Tool<any>[] = [ getPage, getPages, getPageHistory, getRecentChanges, searchPage, searchPageByPrefix, parseWikitext, comparePages, - src/runtime/register.ts:11-31 (registration)The generic register() function that calls server.registerTool() with the tool's name, description, inputSchema, and annotations. This is how compare-pages gets registered with the MCP SDK.
export function register<TSchema extends ZodRawShape, TCtx extends ToolContext>( server: McpServer, tool: Tool<TSchema, TCtx>, handler: (args: z.infer<z.ZodObject<TSchema>>) => Promise<CallToolResult>, ): RegisteredTool { return server.registerTool( tool.name, { description: tool.description, inputSchema: tool.inputSchema, annotations: tool.annotations, }, // The SDK callback signature is `(args, extra) => ...`. Our descriptor // handlers ignore the `extra` parameter, so we widen the type here. The // `ZodRawShape` constraint from zod is the same shape as the SDK's // `ZodRawShapeCompat` (Record<string, AnySchema>) — TypeScript just // can't unify them through the generic boundary. // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- generic boundary; MCP SDK's ToolCallback can't be unified with our typed handler handler as unknown as ToolCallback<TSchema>, ); } - src/results/diffFormat.ts:50-91 (helper)The inlineDiffToText() helper. Parses HTML diff output (from MediaWiki's inline diff format) into a simple text diff with context lines, additions, and deletions. Used by compare-pages to convert the 'body' of the compare response into readable diff text.
export function inlineDiffToText(html: string): string { if (!html) { return ''; } const lines: string[] = []; const rowRegex = /<tr\b[^>]*>([\s\S]*?)<\/tr>/gi; let rowMatch; while ((rowMatch = rowRegex.exec(html)) !== null) { const cells = extractCells(rowMatch[1]); const linenoCell = findCell(cells, 'diff-lineno'); if (linenoCell) { const text = stripTags(linenoCell.inner); const m = text.match(/Line\s+(\d+)/i); if (m) { lines.push(`@@ Line ${m[1]} @@`); } continue; } const contextCell = findCell(cells, 'diff-context'); if (contextCell) { lines.push(' ' + stripTags(contextCell.inner)); continue; } const deleted = findCell(cells, 'diff-deletedline'); const added = findCell(cells, 'diff-addedline'); if (deleted) { lines.push('- ' + stripTags(deleted.inner)); } if (added) { lines.push('+ ' + stripTags(added.inner)); } } return lines.join('\n'); }