Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/core/bin/help.js
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -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)']
],
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(' ')
Expand Down
224 changes: 180 additions & 44 deletions packages/mcp/src/schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,25 +104,64 @@ 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()
)

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()
)
Expand Down Expand Up @@ -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()
)
Expand Down Expand Up @@ -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(),
Expand All @@ -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()
Expand All @@ -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
Expand All @@ -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()

Expand All @@ -348,15 +430,22 @@ 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()

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()
Expand All @@ -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()
})
Expand All @@ -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()
Expand All @@ -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()

Expand All @@ -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()

Expand Down Expand Up @@ -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()

Expand Down
2 changes: 1 addition & 1 deletion packages/mcp/src/tools/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
Expand Down
11 changes: 11 additions & 0 deletions packages/mcp/test/schemas.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading