@microlink/mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +435 -0
- package/bin/microlink-mcp.js +8 -0
- package/package.json +104 -0
- package/scripts/postinstall.js +49 -0
- package/src/index.js +65 -0
- package/src/microlink-client.js +239 -0
- package/src/schemas.js +271 -0
- package/src/tools/audio.js +17 -0
- package/src/tools/extract.js +20 -0
- package/src/tools/index.js +23 -0
- package/src/tools/insights.js +21 -0
- package/src/tools/markdown.js +16 -0
- package/src/tools/meta.js +17 -0
- package/src/tools/palette.js +17 -0
- package/src/tools/pdf.js +23 -0
- package/src/tools/register.js +110 -0
- package/src/tools/screenshot.js +23 -0
- package/src/tools/text.js +16 -0
- package/src/tools/video.js +17 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import mql from '@microlink/mql'
|
|
2
|
+
|
|
3
|
+
const RESPONSE_HEADER_KEYS = [
|
|
4
|
+
'x-request-id',
|
|
5
|
+
'x-pricing-plan',
|
|
6
|
+
'x-cache-status',
|
|
7
|
+
'x-cache-ttl',
|
|
8
|
+
'cf-cache-status',
|
|
9
|
+
'cache-control',
|
|
10
|
+
'x-rate-limit-limit',
|
|
11
|
+
'x-rate-limit-remaining',
|
|
12
|
+
'x-rate-limit-reset',
|
|
13
|
+
'x-response-time',
|
|
14
|
+
'content-type',
|
|
15
|
+
'content-encoding'
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
const FREE_ENDPOINT_ORIGIN = 'https://api.microlink.io'
|
|
19
|
+
const FREE_QUOTA_EXCEEDED_HINT =
|
|
20
|
+
'Free daily quota reached (50 requests/day). Extend your limit by getting an API key at https://microlink.io/#pricing.'
|
|
21
|
+
const EMPTY_OBJECT_MEANS_TRUE_FLAGS = ['screenshot', 'pdf', 'insights']
|
|
22
|
+
|
|
23
|
+
function isPlainObject (value) {
|
|
24
|
+
return Object.prototype.toString.call(value) === '[object Object]'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isObject (value) {
|
|
28
|
+
return value !== null && typeof value === 'object'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function hasSerializableValue (value) {
|
|
32
|
+
if (value === undefined || value === null) {
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (Array.isArray(value)) {
|
|
37
|
+
return value.length > 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (isPlainObject(value)) {
|
|
41
|
+
return Object.values(value).some(hasSerializableValue)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return true
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getHeaderValue (headers, headerName) {
|
|
48
|
+
if (!headers) {
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (typeof headers.get === 'function') {
|
|
53
|
+
const value = headers.get(headerName)
|
|
54
|
+
return value ?? null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (typeof headers !== 'object') {
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const lookup = headerName.toLowerCase()
|
|
62
|
+
|
|
63
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
64
|
+
if (key.toLowerCase() === lookup) {
|
|
65
|
+
return value
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function pickResponseHeaders (headers) {
|
|
73
|
+
const output = {}
|
|
74
|
+
|
|
75
|
+
for (const headerName of RESPONSE_HEADER_KEYS) {
|
|
76
|
+
const value = getHeaderValue(headers, headerName)
|
|
77
|
+
if (value !== null) {
|
|
78
|
+
output[headerName] = value
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return output
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function withForcedFlags (opts, forcedFlags = {}) {
|
|
86
|
+
const nextOpts = { ...opts }
|
|
87
|
+
|
|
88
|
+
// Microlink treats empty objects for these toggles as enabled defaults.
|
|
89
|
+
// Normalize `{}` -> `true` so query serialization always keeps the flag.
|
|
90
|
+
for (const flagName of EMPTY_OBJECT_MEANS_TRUE_FLAGS) {
|
|
91
|
+
if (isPlainObject(nextOpts[flagName]) && !hasSerializableValue(nextOpts[flagName])) {
|
|
92
|
+
nextOpts[flagName] = true
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const [flagName, flagValue] of Object.entries(forcedFlags)) {
|
|
97
|
+
if (!hasSerializableValue(nextOpts[flagName])) {
|
|
98
|
+
nextOpts[flagName] = flagValue
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return nextOpts
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getEndpointFromRequestUrl (requestUrl) {
|
|
106
|
+
try {
|
|
107
|
+
return new URL(requestUrl).origin
|
|
108
|
+
} catch (_) {
|
|
109
|
+
return requestUrl
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function stripResponseFromBody (body) {
|
|
114
|
+
if (!isPlainObject(body)) {
|
|
115
|
+
return body
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const { response, ...payload } = body
|
|
119
|
+
return payload
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function withFreeQuotaHint (message, { requestUrl, statusCode }) {
|
|
123
|
+
const endpoint = getEndpointFromRequestUrl(requestUrl)
|
|
124
|
+
|
|
125
|
+
if (endpoint !== FREE_ENDPOINT_ORIGIN || statusCode !== 429) {
|
|
126
|
+
return message
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (typeof message === 'string' && message.includes('50 requests/day')) {
|
|
130
|
+
return message
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (typeof message === 'string' && message.trim().length > 0) {
|
|
134
|
+
return `${message} ${FREE_QUOTA_EXCEEDED_HINT}`
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return FREE_QUOTA_EXCEEDED_HINT
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function toErrorBody (error, requestUrl, statusCode) {
|
|
141
|
+
if (!isObject(error)) {
|
|
142
|
+
return {
|
|
143
|
+
status: 'error',
|
|
144
|
+
message: withFreeQuotaHint(error instanceof Error ? error.message : String(error), { requestUrl, statusCode }),
|
|
145
|
+
url: requestUrl
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const payload = {}
|
|
150
|
+
|
|
151
|
+
for (const key of ['status', 'data', 'more', 'code', 'id', 'report', 'message', 'url']) {
|
|
152
|
+
if (error[key] !== undefined) {
|
|
153
|
+
payload[key] = error[key]
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (payload.status === undefined) {
|
|
158
|
+
payload.status = 'error'
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (payload.message === undefined) {
|
|
162
|
+
payload.message = error.message || 'Microlink request failed.'
|
|
163
|
+
}
|
|
164
|
+
payload.message = withFreeQuotaHint(payload.message, { requestUrl, statusCode })
|
|
165
|
+
|
|
166
|
+
if (payload.url === undefined) {
|
|
167
|
+
payload.url = requestUrl
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return payload
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function callMicrolink ({ params, forcedFlags = {} }) {
|
|
174
|
+
const { url, apiKey: requestApiKey, ...rawOpts } = params
|
|
175
|
+
const envApiKey = process.env.MICROLINK_API_KEY
|
|
176
|
+
const apiKey = requestApiKey || envApiKey
|
|
177
|
+
const opts = withForcedFlags(rawOpts, forcedFlags)
|
|
178
|
+
|
|
179
|
+
if (apiKey) {
|
|
180
|
+
opts.apiKey = apiKey
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const [requestUrl, requestOpts] = mql.getApiUrl(url, opts, {
|
|
184
|
+
headers: {
|
|
185
|
+
accept: 'application/json'
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
const endpoint = getEndpointFromRequestUrl(requestUrl)
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const body = await mql.fetchFromApi(requestUrl, requestOpts)
|
|
192
|
+
const response = body?.response
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
ok: true,
|
|
196
|
+
statusCode: response?.statusCode ?? 200,
|
|
197
|
+
requestUrl,
|
|
198
|
+
finalUrl: response?.url ?? requestUrl,
|
|
199
|
+
endpoint,
|
|
200
|
+
headers: pickResponseHeaders(response?.headers),
|
|
201
|
+
body: stripResponseFromBody(body)
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
return {
|
|
205
|
+
ok: false,
|
|
206
|
+
statusCode: error?.statusCode ?? 500,
|
|
207
|
+
requestUrl,
|
|
208
|
+
finalUrl: error?.url ?? requestUrl,
|
|
209
|
+
endpoint,
|
|
210
|
+
headers: pickResponseHeaders(error?.headers),
|
|
211
|
+
body: toErrorBody(error, requestUrl, error?.statusCode ?? 500)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function asToolResult (result) {
|
|
217
|
+
const output = {
|
|
218
|
+
endpoint: result.endpoint,
|
|
219
|
+
requestUrl: result.requestUrl,
|
|
220
|
+
finalUrl: result.finalUrl,
|
|
221
|
+
statusCode: result.statusCode,
|
|
222
|
+
responseHeaders: result.headers,
|
|
223
|
+
microlink: result.body
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const isMicrolinkError = result.body?.status && result.body.status !== 'success'
|
|
227
|
+
const isError = !result.ok || isMicrolinkError
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
isError,
|
|
231
|
+
structuredContent: output,
|
|
232
|
+
content: [
|
|
233
|
+
{
|
|
234
|
+
type: 'text',
|
|
235
|
+
text: JSON.stringify(output, null, 2)
|
|
236
|
+
}
|
|
237
|
+
]
|
|
238
|
+
}
|
|
239
|
+
}
|
package/src/schemas.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
const stringOrStringArraySchema = z.union([z.string(), z.array(z.string()).min(1)])
|
|
4
|
+
const stringOrNumberSchema = z.union([z.string(), z.number()])
|
|
5
|
+
|
|
6
|
+
function coerceJsonObjectString (value) {
|
|
7
|
+
if (typeof value !== 'string') {
|
|
8
|
+
return value
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const input = value.trim()
|
|
12
|
+
if (input.length < 2 || !input.startsWith('{') || !input.endsWith('}')) {
|
|
13
|
+
return value
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(input)
|
|
18
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
19
|
+
return parsed
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
// Keep original value and let schema validation surface the error.
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return value
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function coerceBooleanString (value) {
|
|
29
|
+
if (typeof value === 'string') {
|
|
30
|
+
const normalized = value.trim().toLowerCase()
|
|
31
|
+
if (normalized === 'true') return true
|
|
32
|
+
if (normalized === 'false') return false
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return value
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const booleanSchema = z.preprocess(coerceBooleanString, z.boolean())
|
|
39
|
+
const objectLikeSchema = schema => z.preprocess(coerceJsonObjectString, schema)
|
|
40
|
+
const toggledObjectSchema = z.union([booleanSchema, objectLikeSchema(z.object({}).catchall(z.unknown()))])
|
|
41
|
+
const proxySchema = objectLikeSchema(
|
|
42
|
+
z.union([z.string().min(1), z.object({}).catchall(z.unknown())])
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
// A single data-extraction rule: CSS selector(s) + optional attr/type/evaluate/nested data.
|
|
46
|
+
const dataSingleRuleSchema = objectLikeSchema(
|
|
47
|
+
z
|
|
48
|
+
.object({
|
|
49
|
+
selector: z.string().min(1).optional(),
|
|
50
|
+
selectorAll: z.string().min(1).optional(),
|
|
51
|
+
attr: z.string().min(1).optional(),
|
|
52
|
+
type: z.string().min(1).optional(),
|
|
53
|
+
evaluate: z.string().min(1).optional(),
|
|
54
|
+
data: z.record(z.string(), z.unknown()).optional()
|
|
55
|
+
})
|
|
56
|
+
.catchall(z.unknown())
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
// A rule can also be an array of rules used as ordered fallback selectors.
|
|
60
|
+
const dataRuleSchema = z.union([dataSingleRuleSchema, z.array(dataSingleRuleSchema).min(1)])
|
|
61
|
+
|
|
62
|
+
const waitUntilEventSchema = z.enum(['auto', 'load', 'domcontentloaded', 'networkidle0', 'networkidle2'])
|
|
63
|
+
|
|
64
|
+
const viewportSchema = objectLikeSchema(
|
|
65
|
+
z
|
|
66
|
+
.object({
|
|
67
|
+
width: z.number().positive().optional(),
|
|
68
|
+
height: z.number().positive().optional(),
|
|
69
|
+
deviceScaleFactor: z.number().positive().optional(),
|
|
70
|
+
isMobile: booleanSchema.optional(),
|
|
71
|
+
hasTouch: booleanSchema.optional(),
|
|
72
|
+
isLandscape: booleanSchema.optional()
|
|
73
|
+
})
|
|
74
|
+
.strict()
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
const screenshotOverlaySchema = objectLikeSchema(
|
|
78
|
+
z
|
|
79
|
+
.object({
|
|
80
|
+
browser: z.enum(['light', 'dark']).optional(),
|
|
81
|
+
background: z.string().min(1).optional()
|
|
82
|
+
})
|
|
83
|
+
.strict()
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
export const screenshotConfigSchema = objectLikeSchema(
|
|
87
|
+
z
|
|
88
|
+
.object({
|
|
89
|
+
codeScheme: z.string().min(1).optional(),
|
|
90
|
+
element: z.string().min(1).optional(),
|
|
91
|
+
fullPage: booleanSchema.optional(),
|
|
92
|
+
omitBackground: booleanSchema.optional(),
|
|
93
|
+
overlay: screenshotOverlaySchema.optional(),
|
|
94
|
+
type: z.enum(['jpeg', 'png']).optional()
|
|
95
|
+
})
|
|
96
|
+
.strict()
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const pdfMarginSchema = objectLikeSchema(
|
|
100
|
+
z.union([
|
|
101
|
+
z.string().min(1),
|
|
102
|
+
z
|
|
103
|
+
.object({
|
|
104
|
+
top: z.string().min(1).optional(),
|
|
105
|
+
bottom: z.string().min(1).optional(),
|
|
106
|
+
left: z.string().min(1).optional(),
|
|
107
|
+
right: z.string().min(1).optional()
|
|
108
|
+
})
|
|
109
|
+
.strict()
|
|
110
|
+
])
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
export const pdfConfigSchema = objectLikeSchema(
|
|
114
|
+
z
|
|
115
|
+
.object({
|
|
116
|
+
format: z.enum(['Letter', 'Legal', 'Tabloid', 'Ledger', 'A0', 'A1', 'A2', 'A3', 'A4', 'A5', 'A6']).optional(),
|
|
117
|
+
height: z.string().min(1).optional(),
|
|
118
|
+
landscape: booleanSchema.optional(),
|
|
119
|
+
margin: pdfMarginSchema.optional(),
|
|
120
|
+
pageRanges: z.string().min(1).optional(),
|
|
121
|
+
scale: z.number().min(0.1).max(2).optional(),
|
|
122
|
+
width: z.string().min(1).optional()
|
|
123
|
+
})
|
|
124
|
+
.strict()
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
const lighthouseOutputSchema = z.enum(['json', 'html', 'csv'])
|
|
128
|
+
const lighthousePresetSchema = z.enum(['default', 'desktop', 'experimental', 'full', 'lr-desktop', 'lr-mobile', 'perf'])
|
|
129
|
+
|
|
130
|
+
const lighthouseConfigSchema = objectLikeSchema(
|
|
131
|
+
z
|
|
132
|
+
.object({
|
|
133
|
+
output: lighthouseOutputSchema.optional(),
|
|
134
|
+
onlyCategories: stringOrStringArraySchema.optional(),
|
|
135
|
+
preset: lighthousePresetSchema.optional()
|
|
136
|
+
})
|
|
137
|
+
.catchall(z.unknown())
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
export const insightsConfigSchema = objectLikeSchema(
|
|
141
|
+
z
|
|
142
|
+
.object({
|
|
143
|
+
lighthouse: z.union([booleanSchema, lighthouseConfigSchema]).optional(),
|
|
144
|
+
technologies: booleanSchema.optional()
|
|
145
|
+
})
|
|
146
|
+
.strict()
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
export const metaConfigSchema = objectLikeSchema(
|
|
150
|
+
z
|
|
151
|
+
.object({
|
|
152
|
+
author: booleanSchema.optional(),
|
|
153
|
+
date: booleanSchema.optional(),
|
|
154
|
+
description: booleanSchema.optional(),
|
|
155
|
+
image: booleanSchema.optional(),
|
|
156
|
+
lang: booleanSchema.optional(),
|
|
157
|
+
logo: booleanSchema.optional(),
|
|
158
|
+
publisher: booleanSchema.optional(),
|
|
159
|
+
title: booleanSchema.optional(),
|
|
160
|
+
url: booleanSchema.optional()
|
|
161
|
+
})
|
|
162
|
+
.strict()
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
const baseSchema = z.object({
|
|
166
|
+
url: z.string().url(),
|
|
167
|
+
apiKey: z.string().min(1).optional()
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
const fullShape = {
|
|
171
|
+
embed: z.string().min(1).optional(),
|
|
172
|
+
function: z.string().min(1).optional(),
|
|
173
|
+
iframe: toggledObjectSchema.optional(),
|
|
174
|
+
meta: z.union([booleanSchema, metaConfigSchema]).optional(),
|
|
175
|
+
palette: booleanSchema.optional(),
|
|
176
|
+
ping: toggledObjectSchema.optional()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const visualSchema = {
|
|
180
|
+
adblock: booleanSchema.optional(),
|
|
181
|
+
animations: booleanSchema.optional(),
|
|
182
|
+
click: stringOrStringArraySchema.optional(),
|
|
183
|
+
colorScheme: z.enum(['no-preference', 'light', 'dark']).optional(),
|
|
184
|
+
data: objectLikeSchema(z.record(z.string(), dataRuleSchema)).optional(),
|
|
185
|
+
device: z.string().min(1).optional(),
|
|
186
|
+
filename: z.string().min(1).optional(),
|
|
187
|
+
filter: z.string().min(1).optional(),
|
|
188
|
+
force: booleanSchema.optional(),
|
|
189
|
+
headers: objectLikeSchema(z.record(z.string(), z.union([z.string(), z.number(), booleanSchema]))).optional(),
|
|
190
|
+
javascript: booleanSchema.optional(),
|
|
191
|
+
mediaType: z.enum(['screen', 'print']).optional(),
|
|
192
|
+
modules: stringOrStringArraySchema.optional(),
|
|
193
|
+
prerender: z.union([z.literal('auto'), booleanSchema]).optional(),
|
|
194
|
+
proxy: proxySchema.optional(),
|
|
195
|
+
retry: z.number().int().nonnegative().optional(),
|
|
196
|
+
scripts: stringOrStringArraySchema.optional(),
|
|
197
|
+
scroll: z.string().min(1).optional(),
|
|
198
|
+
staleTtl: z.union([z.string(), z.number(), booleanSchema]).optional(),
|
|
199
|
+
styles: stringOrStringArraySchema.optional(),
|
|
200
|
+
timeout: stringOrNumberSchema.optional(),
|
|
201
|
+
ttl: stringOrNumberSchema.optional(),
|
|
202
|
+
viewport: viewportSchema.optional(),
|
|
203
|
+
waitForSelector: z.string().min(1).optional(),
|
|
204
|
+
waitForTimeout: stringOrNumberSchema.optional(),
|
|
205
|
+
waitUntil: z.union([waitUntilEventSchema, z.array(waitUntilEventSchema).min(1)]).optional()
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export const extractInputSchema = baseSchema
|
|
209
|
+
.extend(fullShape)
|
|
210
|
+
.extend(visualSchema)
|
|
211
|
+
.extend({
|
|
212
|
+
audio: booleanSchema.optional(),
|
|
213
|
+
video: booleanSchema.optional(),
|
|
214
|
+
pdf: z.union([booleanSchema, pdfConfigSchema]).optional(),
|
|
215
|
+
screenshot: z.union([booleanSchema, screenshotConfigSchema]).optional(),
|
|
216
|
+
insights: z.union([booleanSchema, insightsConfigSchema]).optional()
|
|
217
|
+
})
|
|
218
|
+
.strict()
|
|
219
|
+
|
|
220
|
+
export const screenshotInputSchema = baseSchema
|
|
221
|
+
.extend(visualSchema)
|
|
222
|
+
.extend({
|
|
223
|
+
screenshot: z.union([booleanSchema, screenshotConfigSchema]).optional()
|
|
224
|
+
})
|
|
225
|
+
.strict()
|
|
226
|
+
|
|
227
|
+
export const pdfInputSchema = baseSchema
|
|
228
|
+
.extend(visualSchema)
|
|
229
|
+
.extend({
|
|
230
|
+
pdf: z.union([booleanSchema, pdfConfigSchema]).optional()
|
|
231
|
+
})
|
|
232
|
+
.strict()
|
|
233
|
+
|
|
234
|
+
export const insightsInputSchema = baseSchema
|
|
235
|
+
.extend({
|
|
236
|
+
insights: z.union([booleanSchema, insightsConfigSchema]).optional()
|
|
237
|
+
})
|
|
238
|
+
.strict()
|
|
239
|
+
|
|
240
|
+
export const audioInputSchema = baseSchema
|
|
241
|
+
.extend({
|
|
242
|
+
proxy: proxySchema.optional(),
|
|
243
|
+
meta: z.union([booleanSchema, metaConfigSchema]).optional(),
|
|
244
|
+
audio: booleanSchema.optional()
|
|
245
|
+
})
|
|
246
|
+
.strict()
|
|
247
|
+
|
|
248
|
+
export const videoInputSchema = baseSchema
|
|
249
|
+
.extend({
|
|
250
|
+
proxy: proxySchema.optional(),
|
|
251
|
+
meta: z.union([booleanSchema, metaConfigSchema]).optional(),
|
|
252
|
+
video: booleanSchema.optional()
|
|
253
|
+
})
|
|
254
|
+
.strict()
|
|
255
|
+
|
|
256
|
+
export const paletteInputSchema = baseSchema
|
|
257
|
+
.extend({
|
|
258
|
+
meta: z.union([booleanSchema, metaConfigSchema]).optional(),
|
|
259
|
+
palette: booleanSchema.optional()
|
|
260
|
+
})
|
|
261
|
+
.strict()
|
|
262
|
+
|
|
263
|
+
export const metaInputSchema = baseSchema
|
|
264
|
+
.extend({
|
|
265
|
+
meta: z.union([booleanSchema, metaConfigSchema]).optional()
|
|
266
|
+
})
|
|
267
|
+
.strict()
|
|
268
|
+
|
|
269
|
+
export const markdownInputSchema = baseSchema.strict()
|
|
270
|
+
|
|
271
|
+
export const textInputSchema = baseSchema.strict()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { audioInputSchema } from '../schemas.js'
|
|
2
|
+
import { register } from './register.js'
|
|
3
|
+
|
|
4
|
+
export function audio (server) {
|
|
5
|
+
register(
|
|
6
|
+
server,
|
|
7
|
+
'microlink_audio',
|
|
8
|
+
[
|
|
9
|
+
'Detect and extract playable audio sources from any URL via Microlink.',
|
|
10
|
+
'Works with SoundCloud, Spotify, Mixcloud, and other audio platforms.',
|
|
11
|
+
'The audio URL is in `data.audio.url`. Also returns `type`, `duration`, `size`, `duration_pretty`, and `size_pretty`.',
|
|
12
|
+
'Combine with `proxy` for sites that require it, or with `meta` to suppress metadata extraction.'
|
|
13
|
+
].join(' '),
|
|
14
|
+
audioInputSchema,
|
|
15
|
+
{ audio: true, meta: false }
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { extractInputSchema } from '../schemas.js'
|
|
2
|
+
import { register } from './register.js'
|
|
3
|
+
|
|
4
|
+
export function extract (server) {
|
|
5
|
+
register(
|
|
6
|
+
server,
|
|
7
|
+
'microlink_extract',
|
|
8
|
+
[
|
|
9
|
+
'Extract structured metadata from any public URL via Microlink.',
|
|
10
|
+
'Returns normalized fields: title, description, author, publisher, date, image, logo, lang, and url.',
|
|
11
|
+
'Use the `data` parameter to scrape custom fields via CSS selectors (selector/selectorAll, attr, type, evaluate).',
|
|
12
|
+
'Combine with `screenshot`, `pdf`, `video`, `audio`, `insights`, `palette`, `iframe` in a single request.',
|
|
13
|
+
'For `screenshot`, `pdf`, and `insights`, pass `true` to enable defaults or pass a config object with options; `{}` is treated as `true`.',
|
|
14
|
+
'Supports device emulation (`device`), custom headers, proxy, JavaScript injection (`scripts`, `modules`, `function`), interaction (`click`, `scroll`), and caching (`ttl`, `staleTtl`, `force`).',
|
|
15
|
+
'The CDN asset URL for any enabled media feature is in `data.<feature>.url` (e.g. `data.screenshot.url`).'
|
|
16
|
+
].join(' '),
|
|
17
|
+
extractInputSchema,
|
|
18
|
+
{}
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { audio } from './audio.js'
|
|
2
|
+
import { extract } from './extract.js'
|
|
3
|
+
import { insights } from './insights.js'
|
|
4
|
+
import { markdown } from './markdown.js'
|
|
5
|
+
import { meta } from './meta.js'
|
|
6
|
+
import { palette } from './palette.js'
|
|
7
|
+
import { pdf } from './pdf.js'
|
|
8
|
+
import { screenshot } from './screenshot.js'
|
|
9
|
+
import { text } from './text.js'
|
|
10
|
+
import { video } from './video.js'
|
|
11
|
+
|
|
12
|
+
export function tools (server) {
|
|
13
|
+
extract(server)
|
|
14
|
+
screenshot(server)
|
|
15
|
+
pdf(server)
|
|
16
|
+
video(server)
|
|
17
|
+
audio(server)
|
|
18
|
+
insights(server)
|
|
19
|
+
meta(server)
|
|
20
|
+
palette(server)
|
|
21
|
+
markdown(server)
|
|
22
|
+
text(server)
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { insightsInputSchema } from '../schemas.js'
|
|
2
|
+
import { register } from './register.js'
|
|
3
|
+
|
|
4
|
+
export function insights (server) {
|
|
5
|
+
register(
|
|
6
|
+
server,
|
|
7
|
+
'microlink_insights',
|
|
8
|
+
[
|
|
9
|
+
'Get web performance and technology-stack insights for any URL via Microlink.',
|
|
10
|
+
'Pass `insights: true` for defaults or `insights: { ... }` for options; `insights: {}` is treated as `true`.',
|
|
11
|
+
'Use `insights.lighthouse` (true or config object) for a Lighthouse performance audit.',
|
|
12
|
+
' - `insights.lighthouse.output`: report format — "json" (default), "html", or "csv".',
|
|
13
|
+
' - `insights.lighthouse.preset`: "default", "desktop", "perf", "experimental", "full", "lr-desktop", "lr-mobile".',
|
|
14
|
+
' - `insights.lighthouse.onlyCategories`: array of category IDs to include (e.g. ["performance", "accessibility"]).',
|
|
15
|
+
'Use `insights.technologies` (true) to detect the technology stack (frameworks, CDNs, analytics, etc.) via Wappalyzer.',
|
|
16
|
+
'Both can be combined in a single request.'
|
|
17
|
+
].join(' '),
|
|
18
|
+
insightsInputSchema,
|
|
19
|
+
{ insights: true, meta: false }
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { markdownInputSchema } from '../schemas.js'
|
|
2
|
+
import { register } from './register.js'
|
|
3
|
+
|
|
4
|
+
export function markdown (server) {
|
|
5
|
+
register(
|
|
6
|
+
server,
|
|
7
|
+
'microlink_markdown',
|
|
8
|
+
[
|
|
9
|
+
'Convert any public URL to Markdown via Microlink.',
|
|
10
|
+
'Returns JSON output with Markdown content under `data.markdown`.',
|
|
11
|
+
'Useful for extracting readable content from web pages, articles, and documentation.'
|
|
12
|
+
].join(' '),
|
|
13
|
+
markdownInputSchema,
|
|
14
|
+
{ data: { markdown: { attr: 'markdown' } }, meta: false }
|
|
15
|
+
)
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { metaInputSchema } from '../schemas.js'
|
|
2
|
+
import { register } from './register.js'
|
|
3
|
+
|
|
4
|
+
export function meta (server) {
|
|
5
|
+
register(
|
|
6
|
+
server,
|
|
7
|
+
'microlink_meta',
|
|
8
|
+
[
|
|
9
|
+
'Extract normalized metadata from any public URL via Microlink.',
|
|
10
|
+
'Returns: `title`, `description`, `lang`, `author`, `publisher`, `date`, `url`, `image` (with dimensions and file info), and `logo` (publisher favicon).',
|
|
11
|
+
'Pass `meta: false` to skip metadata extraction entirely — useful when you only need a screenshot or video and want a faster response.',
|
|
12
|
+
'Pass a config object to include or exclude specific fields: `{ logo: true, title: true }` returns only those fields; `{ image: false }` returns everything except image.'
|
|
13
|
+
].join(' '),
|
|
14
|
+
metaInputSchema,
|
|
15
|
+
{ meta: true }
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { paletteInputSchema } from '../schemas.js'
|
|
2
|
+
import { register } from './register.js'
|
|
3
|
+
|
|
4
|
+
export function palette (server) {
|
|
5
|
+
register(
|
|
6
|
+
server,
|
|
7
|
+
'microlink_palette',
|
|
8
|
+
[
|
|
9
|
+
'Extract a color palette from images detected on any public URL via Microlink.',
|
|
10
|
+
'For each image, returns: `palette` (hex colors from most to least dominant), `background_color` (optimal WCAG-contrast background), `color` (best overlay color for the background), and `alternative_color` (secondary overlay color).',
|
|
11
|
+
'Color data is nested under each image field in the response (e.g. `data.image.palette`, `data.image.background_color`).',
|
|
12
|
+
'Useful for generating design tokens, theming, or accessibility checks from real page images.'
|
|
13
|
+
].join(' '),
|
|
14
|
+
paletteInputSchema,
|
|
15
|
+
{ palette: true, meta: true }
|
|
16
|
+
)
|
|
17
|
+
}
|
package/src/tools/pdf.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { pdfInputSchema } from '../schemas.js'
|
|
2
|
+
import { register } from './register.js'
|
|
3
|
+
|
|
4
|
+
export function pdf (server) {
|
|
5
|
+
register(
|
|
6
|
+
server,
|
|
7
|
+
'microlink_pdf',
|
|
8
|
+
[
|
|
9
|
+
'Generate a PDF of any public URL via Microlink and return a permanent CDN asset URL.',
|
|
10
|
+
'The PDF URL is in `data.pdf.url`.',
|
|
11
|
+
'Pass `pdf: true` for defaults or `pdf: { ... }` for options; `pdf: {}` is treated as `true`.',
|
|
12
|
+
'Use `pdf.format` to set paper size: "A4" (default), "Letter", "Legal", "Tabloid", "Ledger", "A0"–"A6".',
|
|
13
|
+
'Use `pdf.landscape` to switch to landscape orientation.',
|
|
14
|
+
'Use `pdf.margin` to set page margins as a string ("0.35cm") or object with top/bottom/left/right.',
|
|
15
|
+
'Use `pdf.scale` to scale the page (0.1–2.0).',
|
|
16
|
+
'Use `pdf.pageRanges` to select specific pages (e.g. "1-5").',
|
|
17
|
+
'Use `pdf.width` and `pdf.height` for custom dimensions (overrides format).',
|
|
18
|
+
'Combine with `styles`, `scripts`, `modules`, `mediaType`, `waitForSelector`, and `waitUntil` for full control.'
|
|
19
|
+
].join(' '),
|
|
20
|
+
pdfInputSchema,
|
|
21
|
+
{ pdf: true, meta: false }
|
|
22
|
+
)
|
|
23
|
+
}
|