diff --git a/packages/core/bin/help.js b/packages/core/bin/help.js old mode 100644 new mode 100755 index 9d59e2e..cd10479 --- a/packages/core/bin/help.js +++ b/packages/core/bin/help.js @@ -197,7 +197,7 @@ const PRODUCTS = { ['--optimizeForSpeed', 'Faster encode, larger file'], ['--overlay', 'Browser chrome overlay as JSON'], ['--codeScheme', 'Syntax theme for code pages'], - ['--animated', 'Animated screenshot (GIF/MP4)'], + ['--animated', 'Capture a short video (MP4/WebM) instead of a still'], ['--palette', 'Also extract dominant colors'], ['--quality', 'JPEG quality (0–100)'] ], diff --git a/packages/mcp/src/index.js b/packages/mcp/src/index.js index 6a17fc9..f3cd964 100644 --- a/packages/mcp/src/index.js +++ b/packages/mcp/src/index.js @@ -12,7 +12,7 @@ const DEFAULT_INSTRUCTIONS = [ 'Product tools turn a public URL into a Microlink result. Always pass full URLs including the protocol.', "Call microlink_docs({ product }) for a product tool's parameters; do not guess nested options.", 'video/audio return the primary playable asset; videos/audios/links/images collect every match on the page.', - 'extract is custom MQL rules, not full-page markdown/html/text. search takes a query. Checkout tools start and track the purchase of an API key.', + 'extract is custom MQL rules, not full-page markdown/html/text. search takes a query and requires an API key; if the key is missing, use checkout tools so the human can buy one.', 'Without an API key, requests use the free endpoint (50 requests/day); pass apiKey or set MICROLINK_API_KEY for PRO.', 'On failure, read the error message and the hint/reason fields and adjust the request instead of retrying blindly.' ].join(' ') diff --git a/packages/mcp/src/schemas.js b/packages/mcp/src/schemas.js index 39229a7..75fd9e4 100644 --- a/packages/mcp/src/schemas.js +++ b/packages/mcp/src/schemas.js @@ -104,8 +104,15 @@ const viewportSchema = objectLikeSchema( const screenshotOverlaySchema = objectLikeSchema( z .object({ - browser: z.enum(['light', 'dark']).optional(), - background: z.string().min(1).optional() + browser: z + .enum(['light', 'dark']) + .optional() + .describe('Browser chrome theme for the overlay.'), + background: z + .string() + .min(1) + .optional() + .describe('Overlay background color (CSS color).') }) .strict() ) @@ -113,16 +120,48 @@ const screenshotOverlaySchema = objectLikeSchema( export const screenshotConfigSchema = objectLikeSchema( z .object({ - animated: booleanSchema.optional(), - codeScheme: z.string().min(1).optional(), - element: z.string().min(1).optional(), - fullPage: booleanSchema.optional(), - omitBackground: booleanSchema.optional(), - optimizeForSpeed: booleanSchema.optional(), - overlay: screenshotOverlaySchema.optional(), - palette: booleanSchema.optional(), - quality: z.number().min(0).max(100).optional(), - type: z.enum(['jpeg', 'png']).optional() + animated: booleanSchema + .optional() + .describe( + 'Capture an animated screenshot (GIF/MP4) instead of a still.' + ), + codeScheme: z + .string() + .min(1) + .optional() + .describe( + 'Syntax-highlight theme for code pages (for example dracula).' + ), + element: z + .string() + .min(1) + .optional() + .describe('CSS selector of the element to capture.'), + fullPage: booleanSchema + .optional() + .describe('Capture the full scrollable page.'), + omitBackground: booleanSchema + .optional() + .describe('Transparent background (png).'), + optimizeForSpeed: booleanSchema + .optional() + .describe('Faster encode, larger file.'), + overlay: screenshotOverlaySchema + .optional() + .describe('Browser chrome overlay.'), + palette: booleanSchema + .optional() + .describe('Also extract dominant colors.'), + quality: z + .number() + .min(0) + .max(100) + .optional() + .describe('JPEG quality (0–100).'), + type: z + .enum(['jpeg', 'png']) + .optional() + .describe('Image format. Default png.') }) .strict() ) @@ -158,13 +197,24 @@ export const pdfConfigSchema = objectLikeSchema( 'A5', 'A6' ]) - .optional(), - height: z.string().min(1).optional(), - landscape: booleanSchema.optional(), - margin: pdfMarginSchema.optional(), - pageRanges: z.string().min(1).optional(), - scale: z.number().min(0.1).max(2).optional(), - width: z.string().min(1).optional() + .optional() + .describe('Page format. Default A4.'), + height: z + .string() + .min(1) + .optional() + .describe('Page height (CSS length).'), + landscape: booleanSchema.optional().describe('Landscape orientation.'), + margin: pdfMarginSchema + .optional() + .describe('Margin as a CSS length or { top, bottom, left, right }.'), + pageRanges: z + .string() + .min(1) + .optional() + .describe('Pages to print, for example 1-3.'), + scale: z.number().min(0.1).max(2).optional().describe('Scale (0.1–2).'), + width: z.string().min(1).optional().describe('Page width (CSS length).') }) .strict() ) @@ -254,22 +304,33 @@ const browserSchema = { .min(1) .optional() .describe(`Custom cache key for the request. ${PRO}`), - click: stringOrStringArraySchema.optional(), - colorScheme: z.enum(['no-preference', 'light', 'dark']).optional(), - device: z.string().min(1).optional(), + click: stringOrStringArraySchema + .optional() + .describe('CSS selector(s) to click before capture.'), + colorScheme: z + .enum(['no-preference', 'light', 'dark']) + .optional() + .describe('Preferred color scheme.'), + device: z + .string() + .min(1) + .optional() + .describe('Emulate a device, for example iPhone 12.'), filename: z .string() .min(1) .optional() .describe(`Custom name for the generated asset. ${PRO}`), filter: z.string().min(1).optional(), - force: booleanSchema.optional(), + force: booleanSchema.optional().describe('Bypass cache.'), headers: objectLikeSchema( z.record(z.string(), z.union([z.string(), z.number(), booleanSchema])) ) .optional() .describe(`Custom HTTP headers sent to the target URL. ${PRO}`), - javascript: booleanSchema.optional(), + javascript: booleanSchema + .optional() + .describe('Toggle JavaScript execution on the target page.'), mediaType: z.enum(['screen', 'print']).optional(), modules: stringOrStringArraySchema.optional(), prerender: z.union([z.literal('auto'), booleanSchema]).optional(), @@ -280,7 +341,11 @@ const browserSchema = { ), retry: z.number().int().nonnegative().optional(), scripts: stringOrStringArraySchema.optional(), - scroll: z.string().min(1).optional(), + scroll: z + .string() + .min(1) + .optional() + .describe('CSS selector to scroll into view before capture.'), staleTtl: z .union([z.string(), z.number(), booleanSchema]) .optional() @@ -291,16 +356,25 @@ const browserSchema = { timeout: stringOrNumberSchema.optional(), ttl: stringOrNumberSchema.optional(), viewport: viewportSchema.optional(), - waitForSelector: z.string().min(1).optional(), + waitForSelector: z + .string() + .min(1) + .optional() + .describe('Wait for this CSS selector before capture.'), waitForTimeout: stringOrNumberSchema.optional(), waitUntil: z .union([waitUntilEventSchema, z.array(waitUntilEventSchema).min(1)]) .optional() + .describe( + 'Navigation event to wait for: auto, load, domcontentloaded, networkidle0, networkidle2.' + ) } const visualSchema = { ...browserSchema, - data: objectLikeSchema(z.record(z.string(), dataRuleSchema)).optional() + data: objectLikeSchema(z.record(z.string(), dataRuleSchema)) + .optional() + .describe('Custom MQL data rules (selector, attr, type).') } export const extractInputSchema = baseSchema @@ -318,14 +392,22 @@ export const extractInputSchema = baseSchema export const screenshotInputSchema = baseSchema .extend(visualSchema) .extend({ - screenshot: z.union([booleanSchema, screenshotConfigSchema]).optional() + screenshot: z + .union([booleanSchema, screenshotConfigSchema]) + .optional() + .describe( + 'true for defaults, or an object (fullPage, element, type, overlay, animated).' + ) }) .strict() export const pdfInputSchema = baseSchema .extend(visualSchema) .extend({ - pdf: z.union([booleanSchema, pdfConfigSchema]).optional() + pdf: z + .union([booleanSchema, pdfConfigSchema]) + .optional() + .describe('true for defaults, or an object (format, margin, landscape).') }) .strict() @@ -348,7 +430,9 @@ export const videoInputSchema = baseSchema export const logoInputSchema = baseSchema .extend(visualSchema) .extend({ - square: booleanSchema.optional(), + square: booleanSchema + .optional() + .describe('Prefer the square (icon-shaped) logo variant.'), palette: booleanSchema.optional() }) .strict() @@ -356,7 +440,12 @@ export const logoInputSchema = baseSchema export const metadataInputSchema = baseSchema .extend(visualSchema) .extend({ - meta: z.union([booleanSchema, metaConfigSchema]).optional(), + meta: z + .union([booleanSchema, metaConfigSchema]) + .optional() + .describe( + 'true to include metadata, false to skip, or an object to include/exclude fields.' + ), palette: booleanSchema.optional() }) .strict() @@ -365,7 +454,9 @@ export const metadataInputSchema = baseSchema const contentSchema = baseSchema .extend(browserSchema) .extend({ - selector: selectorSchema.optional(), + selector: selectorSchema + .optional() + .describe('CSS selector to scope the extracted content.'), selectorAll: selectorSchema.optional(), type: z.string().min(1).optional() }) @@ -376,8 +467,14 @@ const collectionSchema = baseSchema .extend(browserSchema) .extend({ selector: selectorSchema.optional(), - selectorAll: selectorSchema.optional(), - attr: z.string().min(1).optional(), + selectorAll: selectorSchema + .optional() + .describe('CSS selector(s) matching many nodes.'), + attr: z + .string() + .min(1) + .optional() + .describe('Attribute to read (href, src, ...).'), type: z.string().min(1).optional() }) .strict() @@ -391,8 +488,18 @@ export const textInputSchema = contentSchema export const embedInputSchema = baseSchema .extend(visualSchema) .extend({ - maxWidth: z.coerce.number().int().positive().optional(), - maxHeight: z.coerce.number().int().positive().optional() + maxWidth: z.coerce + .number() + .int() + .positive() + .optional() + .describe('Maximum iframe width in pixels.'), + maxHeight: z.coerce + .number() + .int() + .positive() + .optional() + .describe('Maximum iframe height in pixels.') }) .strict() @@ -411,10 +518,16 @@ export const technologiesInputSchema = baseSchema.extend(visualSchema).strict() export const lighthouseInputSchema = baseSchema .extend(visualSchema) .extend({ - onlyCategories: z.array(z.string().min(1)).optional(), + onlyCategories: z + .array(z.string().min(1)) + .optional() + .describe('Limit the report to these Lighthouse categories.'), onlyAudits: z.array(z.string().min(1)).optional(), skipAudits: z.array(z.string().min(1)).optional(), - output: z.union([z.string().min(1), z.array(z.string().min(1))]).optional() + output: z + .union([z.string().min(1), z.array(z.string().min(1))]) + .optional() + .describe('Report format: json, html, or csv.') }) .strict() @@ -442,18 +555,41 @@ export const searchInputSchema = z 'patents', 'autocomplete' ]) - .optional(), - limit: z.coerce.number().int().positive().optional(), - page: z.coerce.number().int().positive().optional(), - location: z.string().min(1).optional(), - period: z.enum(['hour', 'day', 'week', 'month', 'year']).optional() + .optional() + .describe('Search vertical. Default search.'), + limit: z.coerce + .number() + .int() + .positive() + .optional() + .describe('Maximum number of results.'), + page: z.coerce + .number() + .int() + .positive() + .optional() + .describe('Results page. Default 1.'), + location: z + .string() + .min(1) + .optional() + .describe('Country or locale, for example es.'), + period: z + .enum(['hour', 'day', 'week', 'month', 'year']) + .optional() + .describe('Recency filter.') }) .strict() export const functionInputSchema = baseSchema .extend(visualSchema) .extend({ - code: z.string().min(1) + code: z + .string() + .min(1) + .describe( + 'Function source, for example "async ({ page }) => page.title()".' + ) }) .strict() diff --git a/packages/mcp/src/tools/search.js b/packages/mcp/src/tools/search.js index d56d595..0f0b7ff 100644 --- a/packages/mcp/src/tools/search.js +++ b/packages/mcp/src/tools/search.js @@ -5,7 +5,7 @@ export function search (server) { register( server, 'microlink_search', - 'Google as structured data. Takes `query` (not a URL). Requires an API key.', + 'Google as structured data. Takes `query` (not a URL). Requires an API key; if none, use checkout tools so the human can buy one.', searchInputSchema, (client, { query, ...options }) => client.search(query, options) ) diff --git a/packages/mcp/test/schemas.test.js b/packages/mcp/test/schemas.test.js index 9cbb3f4..119a690 100644 --- a/packages/mcp/test/schemas.test.js +++ b/packages/mcp/test/schemas.test.js @@ -601,6 +601,17 @@ test('text schema rejects unknown top-level keys', () => { ) }) +test('screenshot schema documents fullPage for MCP clients', () => { + const { properties } = z.toJSONSchema(screenshotInputSchema) + const screenshot = properties.screenshot + const fullPage = + screenshot.anyOf?.find(entry => entry.properties?.fullPage)?.properties + ?.fullPage ?? screenshot.properties?.fullPage + + assert.match(screenshot.description, /fullPage/) + assert.match(fullPage.description, /full scrollable page/) +}) + test('PRO parameters are labeled in the JSON Schema shown to MCP clients', () => { const { properties } = z.toJSONSchema(screenshotInputSchema) diff --git a/packages/mcp/test/stdio-server.test.js b/packages/mcp/test/stdio-server.test.js index cc055ba..185c0cb 100644 --- a/packages/mcp/test/stdio-server.test.js +++ b/packages/mcp/test/stdio-server.test.js @@ -17,7 +17,8 @@ test('createMicrolinkServer returns MCP server instance', () => { test('createMicrolinkServer sets default instructions and honors overrides', () => { const withDefaults = createMicrolinkServer() assert.match(withDefaults.server._instructions, /microlink_docs/) - assert.match(withDefaults.server._instructions, /Checkout tools/) + assert.match(withDefaults.server._instructions, /requires an API key/) + assert.match(withDefaults.server._instructions, /checkout tools/) const custom = createMicrolinkServer({ instructions: 'Custom.' }) assert.equal(custom.server._instructions, 'Custom.')