Skip to content
Open
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
34 changes: 34 additions & 0 deletions packages/core/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,39 @@ import createGoogleClient from '@microlink/google'

type GoogleClient = ReturnType<typeof createGoogleClient>

/** Mirrors `@microlink/mql` ActionLocator / Action (kept local to avoid ESM/CJS type import issues). */
type ActionLocator =
| { selector: string }
| { role: string; name?: string }
| { text: string }
| { label: string }
| { placeholder: string }
| { testId: string }
| { alt: string }

export type Action =
| { type: 'inject'; styles?: string[]; scripts?: string[]; modules?: string[] }
| ({ type: 'click' } & ActionLocator)
| ({
type: 'wait'
timeout?: string | number
text?: string
request?: string
visible?: boolean
hidden?: boolean
} & Partial<ActionLocator>)
| ({ type: 'scroll'; x?: number; y?: number } & Partial<ActionLocator>)
| ({ type: 'fill'; value: string } & ActionLocator)
| { type: 'evaluate'; expression: string }
| ({ type: 'screenshot'; fullPage?: boolean } & Partial<ActionLocator>)
| {
type: 'pdf'
format?: string
scale?: number
margin?: string | Record<string, string | number>
printBackground?: boolean
}

/**
* Transport & top-level API query params. Unknown keys fall through
* to the API query string, so an index signature is provided.
Expand All @@ -10,6 +43,7 @@ interface Options {
apiKey?: string
endpoint?: string
headers?: Record<string, string>
actions?: Action[]
adblock?: boolean
animations?: boolean
audio?: boolean
Expand Down
12 changes: 10 additions & 2 deletions packages/core/test/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,19 @@ expectType<Promise<string[]>>(client.audios('https://example.com'))
async function assertions (): Promise<void> {
const screenshot = await client.screenshot('https://example.com', {
fullPage: true,
device: 'iPhone 11'
device: 'iPhone 11',
actions: [
{ type: 'fill', label: 'Email', value: 'user@example.com' },
{ type: 'click', role: 'button', name: 'Sign in' },
{ type: 'wait', text: 'Dashboard' }
]
})
expectType<string>(screenshot.url)

const pdf = await client.pdf('https://example.com', { format: 'A4' })
const pdf = await client.pdf('https://example.com', {
format: 'A4',
actions: [{ type: 'scroll', selector: '#pricing' }]
})
expectType<string>(pdf.url)

const logo = await client.logo('https://example.com', { square: true })
Expand Down
78 changes: 78 additions & 0 deletions packages/mcp/src/schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,10 +228,88 @@ const fullShape = {
ping: toggledObjectSchema.optional()
}

// Shared locator fields for interaction actions (exactly one strategy preferred;
// the API enforces mutual exclusivity — agents may omit and use CSS `selector`).
const locatorFields = {
selector: z.string().min(1).optional(),
role: z.string().min(1).optional(),
name: z.string().min(1).optional(),
text: z.string().min(1).optional(),
label: z.string().min(1).optional(),
placeholder: z.string().min(1).optional(),
testId: z.string().min(1).optional(),
alt: z.string().min(1).optional()
}

const actionSchema = z.discriminatedUnion('type', [
z
.object({
type: z.literal('inject'),
styles: stringOrStringArraySchema.optional(),
scripts: stringOrStringArraySchema.optional(),
modules: stringOrStringArraySchema.optional()
})
.strict(),
z
.object({
type: z.literal('click'),
...locatorFields
})
.strict(),
Comment on lines +253 to +258

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '5,29p' packages/core/src/index.d.ts
sed -n '231,306p' packages/mcp/src/schemas.js

Repository: microlinkhq/microlink

Length of output: 2957


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- locate package files ---'
fd -a '^(package\.json|index\.d\.ts|schemas\.js)$' packages/mcp packages/core | sed 's#^\./##'

printf '%s\n' '--- package metadata for packages/mcp ---'
cat packages/mcp/package.json | sed -n '1,120p'

printf '%s\n' '--- relevant schema sections ---'
sed -n '1,80p' packages/mcp/src/schemas.js
sed -n '215,306p' packages/mcp/src/schemas.js

printf '%s\n' '--- inspect references to locator validation / ActionLocator ---'
rg -n "locatorFields|ActionLocator|mutual|exclusiv|role\?|name" packages -S

Repository: microlinkhq/microlink

Length of output: 42263


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- lockfile zod version references ---'
if [ -f package-lock.json ]; then
  node -e 'const lock=require("./package-lock.json"); for (const [k,v] of Object.entries(lock.packages||{}).filter(([k,v]) => k && v.version)?.entries) if (k.includes("zod")) console.log(`${k}: ${v.version}`)'
fi

printf '%s\n' '--- deterministic schema behavior probe using repo package files ---'
python3 - <<'PY'
from pathlib import Path
import re

schema = Path('packages/mcp/src/schemas.js').read_text()
locator_fields = dict(re.search(r'const locatorFields = \{(?P<body>.*?)\n\}\n(? =const actionSchema)', schema, re.S).group('body'))
click = re.search(r'z\s*\.\s*object\(\{\s*type:\s* z\.literal\('click'\'),(?P<body>.*?)\}\.strict\(\),', schema, re.S)
fill = re.search(r'z\s*\.\s*object\(\{\s*type:\s*z\.literal\('fill'\'),(?P<body>.*?)\}\.strict\(\),', schema, re.S)
core_type = Path('packages/core/src/index.d.ts').read_text()
print('locator_fields_are_optional:', all('optional()' in value for value in locator_fields.values()))
print('click_accepts_no_strategy:', click is not None and '{ type: z.literal("click") }' in click.group(0))
print('fill_accepts_no_strategy:', fill is not None and '{ type: z.literal("fill") }' in fill.group(0))
print('click_accepts_conflicting_selector_text_example:', bool(click and re.search(r'click.*selector.*text', click.group(0), re.S)))
print('fill_accepts_conflicting_selector_text_example:', bool(fill and re.search(r'fill.*selector.*text', fill.group(0), re.S)))
print('core_click_requires_actionlocator:' ,bool(re.search(r'\(\{\s*type:\s*\'click\'\s*\}\s*&\s*ActionLocator\)', core_type)))
print('core_fill_requires_actionlocator:', bool(re.search(r'\(\{\s*type:\s*\'fill\'\s*,\s*value:\s*string\s*\}\s*&\s*ActionLocator\)', core_type)))
PY

Repository: microlinkhq/microlink

Length of output: 514


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

schema = Path('packages/mcp/src/schemas.js').read_text()
locator_body = re.search(r"const locatorFields = \{(?P<body>.*?)\n\}\n", schema, re.S).group('body')
locator_fields = {}
for m in re.finditer(r'(\w+):\s*z\.str.*?\.optional?\(\)', locator_body):
    locator_fields[m.group(1)] = m.group(0).rstrip(',')

click = re.search(r"z\s*\.\s*object\(\{\s*type:\s*z\.literal\('click'\'),(?P<body>.*?)\}\.strict\(\)", schema, re.S, flags=re.S)
fill = re.search(r"z\s*\.\s*object\(\{\s*type:\s*z\.literal\('fill'\'),(?P<body>.*?)\}\.strict\(\)", schema, re.S)
core = Path('packages/core/src/index.d.ts').read_text()

print('all_locator_fields_optional:', all('optional()' in v for v in locator_fields.values()))
print('click_accepts_no_locator_strategy:', bool(click) and '{ type: z.literal('click') }' in click.group(0))
print('fill_accepts_no_locator_strategy:', bool(fill) and '{ type: z.literal('fill') }' in fill.group(0))
print('click_accepts_conflicting_selector_and_text:', bool(click) and 'selector' in click.group(0) and 'text' in click.group(0))
print('fill_accepts_conflicting_selector_and_text:', bool(fill) and 'selector' in fill.group(0) and 'text' in fill.group(0))
print('core_click_requires_actionlocator:', bool(re.search(r"\(\{\s*type:\s*'click'\s*\}\s*&\s*ActionLocator\)", core)))
print('core_fill_requires_actionlocator:', bool(re.search(r"\(\{\s*type:\s*'fill',\s*value:\s*string\s*\}\s*&\s*ActionLocator\)", core)))
PY

Repository: microlinkhq/microlink

Length of output: 413


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

schema = Path('packages/mcp/src/schemas.js').read_text()
locator_body = re.search(r"const locatorFields = \{(?P<body>.*?)\n\}\n", schema, re.S).group('body')
locator_fields = {}
for m in re.finditer(rr"(\w+):\s*z\(.*?\)\.optional\(\)", locator_body):
    locator_fields[m.group(1)] = m.group(0).rstrip(',')

click = re.search(r"z\s*\.\s*object\(\{\s*type:\s*z\.literal\('click'\'),(?P<body>.*?)\}\.strict\(\)", schema, re.S)
fill = re.search(r"z\s*\.\s*object\(\{\s*type:\s*z\.literal\('fill'\'),(?P<body>.*?)\}\.strict\(\)", schema, re.S)
core = Path('packages/core/src/index.d.ts').read_text().splitlines()

print('all_locator_fields_optional:', all('optional()' in v for v in locator_fields.values()))
print('locator_fields', locator_fields)
print('click_accepts_no_locator_strategy:', bool(click) and "'click'" in click.group(0) and '{ type:' not in click.group(1).strip())
print('fill_accepts_no_locator_strategy:', bool(fill))
print('click_accepts_conflicting_selector_and_text:', bool(click) and 'selector' in click.group(0) and 'text' in click.group(0))
print('fill_accepts_conflicting_selector_and_text:', bool(fill) and 'selector' in fill.group(0) and 'text' in fill.group(0))
for i,l in enumerate(core):
    if "type: 'click'" in l or "type: 'fill'" in l or "ActionLocator =" in l:
        print(f'core[{i}]: {l}')
PY

Repository: microlinkhq/microlink

Length of output: 318


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - "$@" <<'PY'
from pathlib import Path
import re

schema = Path('packages/mcp/src/schemas.js').read_text()
locator_body = re.search(r"const locatorFields = \{(?P<body>.*?)\n\}\n", schema, re.S).group('body')
locator_fields = {}
for m in re.finditer(r"(\w+):\s*z\..*?\)\.optional\(\)", locator_body, re.S):
    locator_fields[m.group(1)] = m.group(0).rstrip(',')

click = re.search(r"z\s*\.\s*object\(\{\s*type:\s*z\.literal\('click'\'),(?P<body>.*?)\}\.strict\(\)", schema, re.S)
fill = re.search(r"z\s*\.\s*object\(\{\s*type:\s*z\.literal\('fill'\'),(?P<body>.*?)\}\.strict\(\)", schema, re.S)
core = Path('packages/core/src/index.d.ts').read_text().splitlines()

print('all_locator_fields_optional:', all('optional()' in v for v in locator_fields.values()))
print('locator_fields', locator_fields)
print('click_accepts_no_locator_strategy:', bool(click) and '{ type: z.literal('click') }' in click.group(0))
print('fill_accepts_no_locator_strategy:', bool(fill) and 'value: z.string()' in fill.group(0))
print('click_accepts_conflicting_selector_and_text:', bool(click) and 'selector' in click.group(0) and 'text' in click.group(0))
print('fill_accepts_conflicting_selector_and_text:', bool(fill) and 'selector' in fill.group(0) and 'text' in fill.group(0))
for i,l in enumerate(core):
    if "type: 'click'" in l or "type: 'fill'" in l or "ActionLocator =" in l:
        print(f'core[{i}]: {l}')
PY

Repository: microlinkhq/microlink

Length of output: 413


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 <<'PY'
from pathlib import Path
import re

schema = Path('packages/mcp/src/schemas.js').read_text()
locator_body = re.search(r"const locatorFields = \{(?P<body>.*?)\n\}\n", schema, re.S).group('body')
locator_fields = {}
for m in re.finditer(r"(\w+):\s*z\..*?\)\.optional\(\)", locator_body, re.S):
    locator_fields[m.group(1)] = m.group(0).rstrip(',')

click = re.search(r"z\s*\.\s*object\(\{\s*type:\s*z\.literal\('click'\'),(?P<body>.*?)\}\.strict\(\)", schema, re.S)
fill = re.search(r"z\s*\.\s*object\(\{\s*type:\s*z\.literal\('fill'\'),(?P<body>.*?)\}\.strict\(\)", schema, re.S)
core = Path('packages/core/src/index.d.ts').read_text().splitlines()
action_locator = '\n'.join(core[core.index(next(l for l in core if 'ActionLocator =' in l)): next((i for i,l in enumerate(core) if 'export type Action' in l), len(core))])

print('all_locator_fields_optional=', all('optional()' in v for v in locator_fields.values()))
print('locator_fields=', sorted(locator_fields))
print('click_accepts_no_locator_strategy=', '{ type: z.literal('click') }' in click.group(0) if click else False)
print('click_accepts_conflicting_selector_and_text=', bool(click) and 'selector' in click.group(0) and 'text' in click.group(0))
print('fill_accepts_conflicting_selector_and_text=', bool(fill) and 'selector' in fill.group(0) and 'text' in fill.group(0))
print('core_action_locator_clause=', action_locator[:600].replace('\n',' '))
PY

Repository: microlinkhq/microlink

Length of output: 401


🌐 Web query:

Microlink API ActionLocator click fill schema selector role name text label placeholder testId alt action options

💡 Result:

The term ActionLocator does not exist as a standard parameter or feature in the Microlink API. Microlink uses a declarative, query-string-based API to handle browser automation and data extraction, rather than a locator-based system like those found in testing frameworks such as Playwright [1][2][3]. For browser interactions in Microlink, you instead use specific interaction parameters [2]: * Click: You perform clicks by passing a CSS selector to the click parameter (e.g., click='.my-button') [2]. * Fill/Input: Microlink does not have a dedicated fill parameter for declarative browser interaction. If you need to fill forms or perform complex interactions that go beyond simple clicks or scrolls, you should use the function parameter to run custom JavaScript code with full Puppeteer access [2][4]. * Data Extraction: You define the schema for data you want to extract using the data parameter [5]. This object uses selectors (e.g., selector, selectorAll) and attributes (attr) to target specific elements on a page [5][6]. The attributes you listed (role, name, text, label, placeholder, testId, alt) are standard DOM accessibility or testing attributes commonly used as CSS selectors (e.g., [role='button'], [placeholder='email']) [2]. You can use these as values for the selector fields within your Microlink data rules or click parameters by utilizing standard CSS attribute selector syntax [5][2]. If you are seeing references to ActionLocator, it is likely related to Playwright's internal trace tools or custom implementations in other frameworks rather than the Microlink API itself [7][8][9].

Citations:


🌐 Web query:

packages/mql/src ActionLocator mql index.js "ActionLocator" "click" "fill"

💡 Result:

The terms "ActionLocator", "mql", "index.js", "click", and "fill" appear in several distinct technical contexts, as there is no single standardized library or framework that exclusively combines all these specific components. 1. Playwright and Testing Frameworks: In many automation and testing projects (such as robotframework-browser), "ActionLocator" is a conceptual or class name used to find UI elements [1]. Developers often use Playwright’s.click and.fill methods to interact with these located elements [1][2]. 2. MQL (MetaQuotes Language) Compile Actions: There is a GitHub repository named MQL-Compile-Action which contains an index.js file [3][4]. This project is used to compile MQL files (used in algorithmic trading platforms) and manages this process via build scripts [3]. 3. ObjectStack / UI Frameworks: Some frameworks, like ObjectStack, utilize an ActionLocation concept to define where UI actions (such as buttons or menu items) surface in an application [5][6]. 4. C# Web Frameworks: In custom or educational C# web framework implementations, an ActionLocator component is frequently used within middleware to resolve incoming requests to specific controller actions [7]. 5. Specialized Systems: The term "ActionLocator" also appears in specialized software, such as the UR robot operating system (in the context of command processing) [8] or game development frameworks (like Planeshift) for defining interactive locations [9]. If you are working with a specific codebase or trying to resolve a particular error, you are likely looking at one of these implementations rather than a unified industry-standard package.

Citations:


🌐 Web query:

@microlink/mql ActionLocator click fill selector role name text zod validation

💡 Result:

The @microlink/mql library interacts with the Microlink API to perform web scraping and browser automation [1][2]. Based on official documentation, the concepts of "ActionLocator," "role," "name," and "text" as specific, native parameters for clicking or filling elements are not part of the standard @microlink/mql API [3][4][5]. Instead, Microlink uses the following mechanisms: 1. Interaction (Clicking/Filling): The click parameter is used for interaction before data extraction [6]. It accepts a CSS selector as a string (or an array of strings) to target the elements to be clicked [6][3]. Example: const { data } = await mql('https://example.com', { click: '.button-selector' }) 2. Data Extraction (Selectors): Data extraction is defined via the data parameter, which uses CSS selectors [4][7]. - selector: Specifies the target element (CSS selector) [8][5]. - attr: Specifies the attribute to extract, such as 'text', 'href', 'src', or 'html' [8][9][5]. - type: Used for validation and normalization of the extracted value (e.g., 'string', 'number', 'url', 'image') [10][9][5]. 3. Validation: Microlink does not natively integrate Zod for validation within the request configuration [10][5]. Instead, it uses a built-in type property in the data extraction rule to ensure the extracted value matches a specific format (e.g., 'number', 'date') [10][9]. If the value does not match the expected type, it is considered invalid and may be discarded [10][11]. Note: Some third-party agentic frameworks or automation wrappers (like the example found in the Catcher repository) may implement higher-level abstractions using terms like "role", "name", and "text" to construct Playwright locators internally, but these are not native features of the @microlink/mql library itself [12].

Citations:


Require one locator strategy for click and fill.

The locatorFields object makes every locator field optional, so these action schemas accept targetless actions and compatible conflicting strategies such as selector plus text. This diverges from ActionLocator, which allows only one strategy. Add a refinement for both schemas that requires exactly one strategy; treat role plus optional name as one strategy, and reject name when role is absent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp/src/schemas.js` around lines 253 - 258, The click and fill
schemas currently allow missing or conflicting locator strategies. Add
refinements to both action schemas requiring exactly one strategy, counting role
with optional name as a single strategy and rejecting name without role;
preserve the existing locator field definitions and strictness.

z
.object({
type: z.literal('wait'),
timeout: stringOrNumberSchema.optional(),
request: z.string().min(1).optional(),
visible: booleanSchema.optional(),
hidden: booleanSchema.optional(),
...locatorFields
})
.strict(),
z
.object({
type: z.literal('scroll'),
x: z.number().optional(),
y: z.number().optional(),
...locatorFields
})
.strict(),
z
.object({
type: z.literal('fill'),
value: z.string(),
...locatorFields
})
.strict(),
z
.object({
type: z.literal('evaluate'),
expression: z.string().min(1)
})
.strict(),
z
.object({
type: z.literal('screenshot'),
fullPage: booleanSchema.optional(),
...locatorFields
})
.strict(),
z
.object({
type: z.literal('pdf'),
format: z.string().min(1).optional(),
scale: z.number().optional(),
margin: pdfMarginSchema.optional(),
printBackground: booleanSchema.optional()
})
.strict()
])

// Shared Microlink API query parameters (see microlink.io/docs/api/parameters).
// Product tools layer their own fields on top; these apply to any URL fetch.
// `data` is separate: content/collection helpers overwrite it with their field rule.
const browserSchema = {
actions: z.array(actionSchema).min(1).optional(),
adblock: booleanSchema.optional(),
animations: booleanSchema.optional(),
cacheKey: z.string().min(1).optional(),
Expand Down
3 changes: 2 additions & 1 deletion packages/mcp/src/tools/function.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export function fn (server) {
[
'Run a JavaScript function against any public URL inside Microlink’s server-side browser sandbox.',
'Pass `code` as the function source (e.g. "async ({ page }) => page.title()"); it receives `{ page, response, ...args }` and its return value comes back in `value`.',
'Combine with browser options such as `javascript`, `waitUntil`, `waitForSelector`, `click`, `scroll`, `headers`, and `proxy`.',
'Prefer `actions` for ordered interactions before the function runs; legacy `waitForSelector`, `click`, and `scroll` still work.',
'Also combine with `javascript`, `waitUntil`, `headers`, and `proxy`.',
'Also returns `isFulfilled`, `profiling`, and `logging`. Mirrors the `microlink.function(url, code)` library method.'
].join(' '),
functionInputSchema,
Expand Down
3 changes: 2 additions & 1 deletion packages/mcp/src/tools/html.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export function html (server) {
[
'Extract the HTML content of any public URL via Microlink.',
'Returns the page HTML as a string. Pass `selector` to scope it to part of the page.',
'Combine with browser options such as `javascript`, `waitUntil`, `waitForSelector`, `headers`, and `proxy`.',
'Prefer `actions` for ordered interactions (click, wait, fill, …); legacy `waitForSelector` still works.',
'Also combine with `javascript`, `waitUntil`, `headers`, and `proxy`.',
'Mirrors the `microlink.html(url)` library method.'
].join(' '),
htmlInputSchema,
Expand Down
3 changes: 2 additions & 1 deletion packages/mcp/src/tools/pdf.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ export function pdf (server) {
'Generate a PDF of any public URL via Microlink and return the asset object (`url`, `type`, `size`, ...) with a permanent CDN URL.',
'Pass `pdf: true` for defaults or `pdf: { ... }` for options; `pdf: {}` is treated as `true`.',
'Use `pdf.format` ("A4" default, "Letter", "Legal", ...), `pdf.landscape`, `pdf.margin` (string or top/bottom/left/right object), `pdf.scale` (0.1-2.0), `pdf.pageRanges` ("1-5"), or `pdf.width`/`pdf.height`.',
'Combine with `styles`, `scripts`, `modules`, `mediaType`, `waitForSelector`, and `waitUntil` for full control.',
'Prefer `actions` (ordered browser steps: inject, click, wait, scroll, fill, pdf, …) with semantic locators or CSS `selector`.',
'Legacy `styles`, `scripts`, `modules`, `waitForSelector`, and `waitUntil` still work; also `mediaType`.',
'Mirrors the `microlink.pdf(url, options)` library method.'
].join(' '),
pdfInputSchema,
Expand Down
3 changes: 2 additions & 1 deletion packages/mcp/src/tools/screenshot.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export function screenshot (server) {
'Use `screenshot.fullPage` to capture the whole scrollable page.',
'Use `screenshot.animated` to capture an animated screenshot (GIF/MP4) instead of a still image.',
'Use `screenshot.element` (CSS selector) to capture a specific element, `screenshot.type` for format ("jpeg", default "png"), `screenshot.omitBackground` for transparency, `screenshot.overlay` for browser chrome, `screenshot.palette` to also extract dominant colors, or `screenshot.codeScheme` to theme code pages.',
'Combine with `device`, `viewport`, `click`, `scroll`, `styles`, `scripts`, `modules`, `waitForSelector`, `waitForTimeout`, `waitUntil`, `colorScheme`, and `mediaType`.',
'Prefer `actions` (ordered browser steps: inject, click, wait, scroll, fill, screenshot, …) with semantic locators (`role`+`name`, `label`, `text`, `testId`) or CSS `selector` — e.g. `actions: [{ type: "click", role: "button", name: "Accept" }, { type: "wait", timeout: "1s" }]`.',
'Legacy `click`, `scroll`, `styles`, `scripts`, `modules`, `waitForSelector`, and `waitForTimeout` still work; also `device`, `viewport`, `waitUntil`, `colorScheme`, and `mediaType`.',
'Mirrors the `microlink.screenshot(url, options)` library method.'
].join(' '),
screenshotInputSchema,
Expand Down
42 changes: 42 additions & 0 deletions packages/mcp/test/schemas.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,48 @@ test('function schema accepts code with shared browser options', () => {
assert.equal(result.data.click, '#accept')
})

test('screenshot schema accepts actions with semantic locators', () => {
const result = screenshotInputSchema.safeParse({
url: 'https://app.example.com/login',
screenshot: true,
actions: [
{ type: 'fill', label: 'Email', value: 'user@example.com' },
{ type: 'click', role: 'button', name: 'Sign in' },
{ type: 'wait', text: 'Dashboard' },
{ type: 'screenshot', fullPage: true }
]
})

assert.equal(result.success, true)
assert.equal(result.data.actions.length, 4)
assert.equal(result.data.actions[0].type, 'fill')
assert.equal(result.data.actions[1].role, 'button')
})

test('screenshot schema rejects actions with unknown type', () => {
const result = screenshotInputSchema.safeParse({
url: 'https://microlink.io',
actions: [{ type: 'drag', selector: '#box' }]
})

assert.equal(result.success, false)
})

test('pdf schema accepts actions with inject and wait', () => {
const result = pdfInputSchema.safeParse({
url: 'https://microlink.io',
pdf: true,
actions: [
{ type: 'inject', styles: ['.banner { display: none }'] },
{ type: 'wait', timeout: '1s' },
{ type: 'pdf', format: 'A4' }
]
})

assert.equal(result.success, true)
assert.equal(result.data.actions[0].type, 'inject')
})

test('metadata schema accepts palette and waitUntil', () => {
const result = metadataInputSchema.safeParse({
url: 'https://microlink.io',
Expand Down
33 changes: 33 additions & 0 deletions packages/mql/dist/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,38 @@ type ScreenshotOptions = {
type?: 'jpeg' | 'png'
}

export type ActionLocator =
| { selector: string }
| { role: string; name?: string }
| { text: string }
| { label: string }
| { placeholder: string }
| { testId: string }
| { alt: string }

export type Action =
| { type: 'inject'; styles?: string[]; scripts?: string[]; modules?: string[] }
| ({ type: 'click' } & ActionLocator)
| ({
type: 'wait'
timeout?: string | number
text?: string
request?: string
visible?: boolean
hidden?: boolean
} & Partial<ActionLocator>)
| ({ type: 'scroll'; x?: number; y?: number } & Partial<ActionLocator>)
| ({ type: 'fill'; value: string } & ActionLocator)
| { type: 'evaluate'; expression: string }
| ({ type: 'screenshot'; fullPage?: boolean } & Partial<ActionLocator>)
| {
type: 'pdf'
format?: string
scale?: number
margin?: string | PdfMargin
printBackground?: boolean
}

type MqlClientOptions = {
apiKey?: string
endpoint?: string
Expand Down Expand Up @@ -78,6 +110,7 @@ type MqlQueryOptions = {
}

export type MicrolinkApiOptions = {
actions?: Action[]
adblock?: boolean
animations?: boolean
audio?: boolean
Expand Down
16 changes: 16 additions & 0 deletions packages/mql/test/get-api-url.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,19 @@ test('undefined', t => {
})
)
})

test('actions flatten to dotted keys', t => {
t.snapshot(
mql.getApiUrl('https://app.example.com/login', {
meta: false,
screenshot: true,
actions: [
{ type: 'fill', label: 'Email', value: 'user@example.com' },
{ type: 'fill', label: 'Password', value: 'secret' },
{ type: 'click', role: 'button', name: 'Sign in' },
{ type: 'wait', text: 'Dashboard' },
{ type: 'screenshot', fullPage: true }
]
})
)
})
20 changes: 20 additions & 0 deletions packages/mql/test/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,26 @@ mql('https://example.com', {
}
})

/** actions */

mql('https://example.com', {
meta: false,
screenshot: true,
actions: [
{ type: 'inject', styles: ['.banner { display: none }'] },
{ type: 'fill', label: 'Email', value: 'user@example.com' },
{ type: 'fill', label: 'Password', value: 'secret' },
{ type: 'click', role: 'button', name: 'Sign in' },
{ type: 'wait', text: 'Dashboard' },
{ type: 'wait', timeout: '3s' },
{ type: 'wait', request: '*api.example.com/user*' },
{ type: 'scroll', selector: '#pricing' },
{ type: 'screenshot', fullPage: true },
{ type: 'pdf', format: 'A4' },
{ type: 'evaluate', expression: 'window.ready === true' }
]
})

/** others */

mql('https://example.com', { click: ['div'] })
Expand Down
Loading
Loading