@microlink/mcp 2.2.0 → 2.4.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/README.md +2 -2
- package/package.json +2 -2
- package/src/microlink-client.js +59 -4
- package/src/output-schemas.js +108 -0
- package/src/schemas.js +54 -9
- package/src/tools/register.js +9 -1
package/README.md
CHANGED
|
@@ -161,7 +161,7 @@ Each tool is a thin wrapper over a [`microlink.io`](https://github.com/microlink
|
|
|
161
161
|
### Response shape
|
|
162
162
|
|
|
163
163
|
- Each tool returns the library's **direct result** under `structuredContent.data` (and the same value as pretty-printed JSON text). For example `microlink_markdown` → `{ data: "# Title\n..." }`, `microlink_screenshot` → `{ data: { url, type, width, height, size } }`, `microlink_links` → `{ data: ["https://...", ...] }`.
|
|
164
|
-
- On failure the tool sets MCP `isError` and returns `{ error: { message, code?, status?, statusCode?, url?, more? } }
|
|
164
|
+
- On failure the tool sets MCP `isError` and returns `{ error: { message, code?, status?, statusCode?, url?, more?, details? } }`, where `message` carries the specific cause reported by the API. Capability errors that retrying cannot fix (for example `EPROXYNEEDED` or `EINTEGRATION`) also include machine-readable `reason` (`upgrade_required`), `capability`, an `upgrade` object with the plan and pricing URL, and an agent-facing `hint` with the next step. A `429` includes `reason: "quota_exceeded"` and a free-quota `hint`.
|
|
165
165
|
|
|
166
166
|
Parameters labeled `PRO` in the official Microlink docs require a paid plan.
|
|
167
167
|
For compatibility with some MCP clients:
|
|
@@ -522,7 +522,7 @@ The `MICROLINK_API_KEY` environment variable is the recommended approach for mos
|
|
|
522
522
|
|
|
523
523
|
If an API key is present, requests are sent to `https://pro.microlink.io`; otherwise they go to `https://api.microlink.io` (free endpoint).
|
|
524
524
|
|
|
525
|
-
When the free endpoint returns `429`, this MCP adds a clear hint in the tool error message: free daily quota reached
|
|
525
|
+
When the free endpoint returns `429`, this MCP adds a clear hint in the tool error message: free daily quota reached, plus upgrade/API key guidance at [microlink.io/#pricing](https://microlink.io/#pricing).
|
|
526
526
|
|
|
527
527
|
## License
|
|
528
528
|
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@microlink/mcp",
|
|
3
3
|
"description": "MCP server for Microlink API",
|
|
4
4
|
"homepage": "https://github.com/microlinkhq/microlink",
|
|
5
|
-
"version": "2.
|
|
5
|
+
"version": "2.4.0",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/index.js"
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
"access": "public"
|
|
67
67
|
},
|
|
68
68
|
"type": "module",
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "de05659e0fe9d49ba25643efbcb9914d5332ca91"
|
|
70
70
|
}
|
package/src/microlink-client.js
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
import createClient, { MicrolinkError } from 'microlink.io'
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
3
|
+
const UPGRADE_URL = 'https://microlink.io/#pricing'
|
|
4
|
+
|
|
5
|
+
const FREE_QUOTA_EXCEEDED_HINT = `Free daily quota reached. Extend your limit by getting an API key at ${UPGRADE_URL}.`
|
|
6
|
+
|
|
7
|
+
// Actionable guidance for capability errors that retrying cannot fix:
|
|
8
|
+
// what happened, why, and how to continue. `reason` and `capability` are
|
|
9
|
+
// machine-readable so clients can react programmatically; `hint` is the
|
|
10
|
+
// agent-facing next step.
|
|
11
|
+
const ERROR_GUIDANCE = {
|
|
12
|
+
EPROXYNEEDED: {
|
|
13
|
+
reason: 'upgrade_required',
|
|
14
|
+
capability: 'proxy',
|
|
15
|
+
hint: `The target website is behind antibot protection and needs the Microlink proxy network, included in PRO plans. The request is correct: get an API key at ${UPGRADE_URL}, pass it as \`apiKey\` (or set MICROLINK_API_KEY) and repeat the same call.`
|
|
16
|
+
},
|
|
17
|
+
EINTEGRATION: {
|
|
18
|
+
reason: 'upgrade_required',
|
|
19
|
+
capability: 'integration',
|
|
20
|
+
hint: `This capability requires a PRO plan. Get an API key at ${UPGRADE_URL}, pass it as \`apiKey\` (or set MICROLINK_API_KEY) and repeat the same call.`
|
|
21
|
+
}
|
|
22
|
+
}
|
|
5
23
|
|
|
6
24
|
// A single shared client; the per-request apiKey travels in the options bag.
|
|
7
25
|
export const client = createClient()
|
|
@@ -25,13 +43,38 @@ export function asToolResult (value) {
|
|
|
25
43
|
}
|
|
26
44
|
}
|
|
27
45
|
|
|
46
|
+
const isPlainObject = value =>
|
|
47
|
+
value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
48
|
+
|
|
49
|
+
// The API wraps some failures in a generic top-level message ("The request
|
|
50
|
+
// has been not processed…"), while the specific cause travels in `data`
|
|
51
|
+
// (e.g. `data.url`). Only then should the data-derived message lead: a
|
|
52
|
+
// specific description is the real cause and must not be hidden by
|
|
53
|
+
// auxiliary strings in `data`.
|
|
54
|
+
const GENERIC_API_MESSAGE = 'The request has been not processed.'
|
|
55
|
+
|
|
28
56
|
export function asErrorResult (error) {
|
|
29
57
|
const isMql = error instanceof MicrolinkError
|
|
30
58
|
const statusCode = isMql ? error.statusCode : undefined
|
|
59
|
+
const description = isMql ? error.description : undefined
|
|
60
|
+
|
|
61
|
+
const details = isMql && isPlainObject(error.data) ? error.data : undefined
|
|
62
|
+
const detailMessage =
|
|
63
|
+
details &&
|
|
64
|
+
Object.values(details)
|
|
65
|
+
.filter(value => typeof value === 'string')
|
|
66
|
+
.join(' ')
|
|
67
|
+
|
|
68
|
+
const isGenericDescription =
|
|
69
|
+
typeof description === 'string' &&
|
|
70
|
+
description.startsWith(GENERIC_API_MESSAGE)
|
|
31
71
|
|
|
32
72
|
const payload = {
|
|
33
73
|
message:
|
|
34
|
-
(
|
|
74
|
+
(isGenericDescription ? detailMessage : undefined) ||
|
|
75
|
+
description ||
|
|
76
|
+
error?.message ||
|
|
77
|
+
String(error)
|
|
35
78
|
}
|
|
36
79
|
|
|
37
80
|
if (isMql) {
|
|
@@ -40,9 +83,21 @@ export function asErrorResult (error) {
|
|
|
40
83
|
if (statusCode) payload.statusCode = statusCode
|
|
41
84
|
if (error.url) payload.url = error.url
|
|
42
85
|
if (error.more) payload.more = error.more
|
|
86
|
+
if (details) payload.details = details
|
|
43
87
|
}
|
|
44
88
|
|
|
45
|
-
|
|
89
|
+
const guidance = isMql && ERROR_GUIDANCE[error.code]
|
|
90
|
+
if (guidance) {
|
|
91
|
+
payload.reason = guidance.reason
|
|
92
|
+
payload.capability = guidance.capability
|
|
93
|
+
payload.hint = guidance.hint
|
|
94
|
+
payload.upgrade = { plan: 'pro', url: UPGRADE_URL }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (statusCode === 429) {
|
|
98
|
+
payload.reason = 'quota_exceeded'
|
|
99
|
+
payload.hint = FREE_QUOTA_EXCEEDED_HINT
|
|
100
|
+
}
|
|
46
101
|
|
|
47
102
|
return {
|
|
48
103
|
isError: true,
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
// Output schemas for every tool's `structuredContent.data` value.
|
|
4
|
+
// They mirror the TypeScript definitions that ship with the library
|
|
5
|
+
// (packages/core/src/index.d.ts, packages/search/src/index.d.ts), which are
|
|
6
|
+
// the canonical contract for response shapes. Index signatures map to
|
|
7
|
+
// `.catchall(z.unknown())` so forward-compatible API fields always validate.
|
|
8
|
+
|
|
9
|
+
// `Asset` (packages/core/src/index.d.ts).
|
|
10
|
+
const assetSchema = z
|
|
11
|
+
.object({
|
|
12
|
+
url: z.string(),
|
|
13
|
+
type: z.string().optional(),
|
|
14
|
+
size: z.number().optional(),
|
|
15
|
+
size_pretty: z.string().optional(),
|
|
16
|
+
width: z.number().optional(),
|
|
17
|
+
height: z.number().optional()
|
|
18
|
+
})
|
|
19
|
+
.catchall(z.unknown())
|
|
20
|
+
|
|
21
|
+
// logo/video/audio return `Asset | null`: the primary media field is null
|
|
22
|
+
// when the page has no detectable asset.
|
|
23
|
+
const nullableAssetSchema = assetSchema.nullable()
|
|
24
|
+
|
|
25
|
+
// `Metadata` (packages/core/src/index.d.ts).
|
|
26
|
+
const metadataSchema = z
|
|
27
|
+
.object({
|
|
28
|
+
title: z.string().nullable().optional(),
|
|
29
|
+
description: z.string().nullable().optional(),
|
|
30
|
+
url: z.string().nullable().optional()
|
|
31
|
+
})
|
|
32
|
+
.catchall(z.unknown())
|
|
33
|
+
|
|
34
|
+
// `Embed` (packages/core/src/index.d.ts). `embed()` returns `data.iframe`
|
|
35
|
+
// verbatim; a successful response omits it when oEmbed discovery fails,
|
|
36
|
+
// and asToolResult stores that as null.
|
|
37
|
+
const embedSchema = z
|
|
38
|
+
.object({
|
|
39
|
+
html: z.string(),
|
|
40
|
+
scripts: z.array(z.unknown()).optional()
|
|
41
|
+
})
|
|
42
|
+
.catchall(z.unknown())
|
|
43
|
+
.nullable()
|
|
44
|
+
|
|
45
|
+
// `FunctionResult<unknown>` (packages/core/src/index.d.ts); profiling and
|
|
46
|
+
// logging are optional because the API may omit them.
|
|
47
|
+
const functionResultSchema = z
|
|
48
|
+
.object({
|
|
49
|
+
isFulfilled: z.boolean(),
|
|
50
|
+
value: z.unknown(),
|
|
51
|
+
profiling: z.record(z.string(), z.unknown()).optional(),
|
|
52
|
+
logging: z.record(z.string(), z.unknown()).optional()
|
|
53
|
+
})
|
|
54
|
+
.catchall(z.unknown())
|
|
55
|
+
|
|
56
|
+
// Search pages (packages/search/src/index.d.ts) projected to JSON: the lazy
|
|
57
|
+
// helpers (`html()`, `markdown()`, `next()`) are functions and never reach
|
|
58
|
+
// the wire. One tolerant shape covers every vertical, including autocomplete
|
|
59
|
+
// (`{ value }` results).
|
|
60
|
+
const searchResultSchema = z
|
|
61
|
+
.object({
|
|
62
|
+
title: z.string().optional(),
|
|
63
|
+
url: z.string().optional(),
|
|
64
|
+
description: z.string().optional(),
|
|
65
|
+
value: z.string().optional()
|
|
66
|
+
})
|
|
67
|
+
.catchall(z.unknown())
|
|
68
|
+
|
|
69
|
+
const searchPageSchema = z
|
|
70
|
+
.object({
|
|
71
|
+
results: z.array(searchResultSchema),
|
|
72
|
+
knowledgeGraph: z.record(z.string(), z.unknown()).optional(),
|
|
73
|
+
peopleAlsoAsk: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
74
|
+
relatedSearches: z.array(z.record(z.string(), z.unknown())).optional()
|
|
75
|
+
})
|
|
76
|
+
.catchall(z.unknown())
|
|
77
|
+
|
|
78
|
+
// Content rules return null when the selector matches nothing
|
|
79
|
+
// (verified against the live API: data.markdown/text are null on no match).
|
|
80
|
+
const stringSchema = z.string().nullable()
|
|
81
|
+
const stringArraySchema = z.array(z.string())
|
|
82
|
+
const recordSchema = z.record(z.string(), z.unknown())
|
|
83
|
+
const unknownArraySchema = z.array(z.unknown())
|
|
84
|
+
// Default Lighthouse JSON is an object; `output: 'html' | 'csv'` returns a string.
|
|
85
|
+
const lighthouseSchema = z.union([z.string(), recordSchema])
|
|
86
|
+
|
|
87
|
+
export const outputSchemas = {
|
|
88
|
+
metadata: metadataSchema,
|
|
89
|
+
logo: nullableAssetSchema,
|
|
90
|
+
markdown: stringSchema,
|
|
91
|
+
html: stringSchema,
|
|
92
|
+
text: stringSchema,
|
|
93
|
+
screenshot: assetSchema,
|
|
94
|
+
pdf: assetSchema,
|
|
95
|
+
embed: embedSchema,
|
|
96
|
+
video: nullableAssetSchema,
|
|
97
|
+
audio: nullableAssetSchema,
|
|
98
|
+
links: stringArraySchema,
|
|
99
|
+
images: stringArraySchema,
|
|
100
|
+
videos: stringArraySchema,
|
|
101
|
+
audios: stringArraySchema,
|
|
102
|
+
emails: stringArraySchema,
|
|
103
|
+
technologies: unknownArraySchema,
|
|
104
|
+
lighthouse: lighthouseSchema,
|
|
105
|
+
search: searchPageSchema,
|
|
106
|
+
function: functionResultSchema,
|
|
107
|
+
extract: recordSchema
|
|
108
|
+
}
|
package/src/schemas.js
CHANGED
|
@@ -215,8 +215,19 @@ export const metaConfigSchema = objectLikeSchema(
|
|
|
215
215
|
)
|
|
216
216
|
|
|
217
217
|
const baseSchema = z.object({
|
|
218
|
-
url: z
|
|
219
|
-
|
|
218
|
+
url: z
|
|
219
|
+
.string()
|
|
220
|
+
.url()
|
|
221
|
+
.describe(
|
|
222
|
+
'Public URL of the page to process. Include the protocol, for example https://example.com.'
|
|
223
|
+
),
|
|
224
|
+
apiKey: z
|
|
225
|
+
.string()
|
|
226
|
+
.min(1)
|
|
227
|
+
.optional()
|
|
228
|
+
.describe(
|
|
229
|
+
'Microlink PRO API key. Omit it unless you have one: requests then use the MICROLINK_API_KEY environment variable or the free endpoint.'
|
|
230
|
+
)
|
|
220
231
|
})
|
|
221
232
|
|
|
222
233
|
const fullShape = {
|
|
@@ -231,28 +242,51 @@ const fullShape = {
|
|
|
231
242
|
// Shared Microlink API query parameters (see microlink.io/docs/api/parameters).
|
|
232
243
|
// Product tools layer their own fields on top; these apply to any URL fetch.
|
|
233
244
|
// `data` is separate: content/collection helpers overwrite it with their field rule.
|
|
245
|
+
// PRO parameters per the Microlink API spec (https://microlink.io/openapi.json):
|
|
246
|
+
// they only take effect with an API key attached to the request.
|
|
247
|
+
const PRO = 'PRO: requires a Microlink API key (Pro plan).'
|
|
248
|
+
|
|
234
249
|
const browserSchema = {
|
|
235
250
|
adblock: booleanSchema.optional(),
|
|
236
251
|
animations: booleanSchema.optional(),
|
|
237
|
-
cacheKey: z
|
|
252
|
+
cacheKey: z
|
|
253
|
+
.string()
|
|
254
|
+
.min(1)
|
|
255
|
+
.optional()
|
|
256
|
+
.describe(`Custom cache key for the request. ${PRO}`),
|
|
238
257
|
click: stringOrStringArraySchema.optional(),
|
|
239
258
|
colorScheme: z.enum(['no-preference', 'light', 'dark']).optional(),
|
|
240
259
|
device: z.string().min(1).optional(),
|
|
241
|
-
filename: z
|
|
260
|
+
filename: z
|
|
261
|
+
.string()
|
|
262
|
+
.min(1)
|
|
263
|
+
.optional()
|
|
264
|
+
.describe(`Custom name for the generated asset. ${PRO}`),
|
|
242
265
|
filter: z.string().min(1).optional(),
|
|
243
266
|
force: booleanSchema.optional(),
|
|
244
267
|
headers: objectLikeSchema(
|
|
245
268
|
z.record(z.string(), z.union([z.string(), z.number(), booleanSchema]))
|
|
246
|
-
)
|
|
269
|
+
)
|
|
270
|
+
.optional()
|
|
271
|
+
.describe(`Custom HTTP headers sent to the target URL. ${PRO}`),
|
|
247
272
|
javascript: booleanSchema.optional(),
|
|
248
273
|
mediaType: z.enum(['screen', 'print']).optional(),
|
|
249
274
|
modules: stringOrStringArraySchema.optional(),
|
|
250
275
|
prerender: z.union([z.literal('auto'), booleanSchema]).optional(),
|
|
251
|
-
proxy: proxySchema
|
|
276
|
+
proxy: proxySchema
|
|
277
|
+
.optional()
|
|
278
|
+
.describe(
|
|
279
|
+
`Proxy rotation to bypass IP rate limits, CAPTCHAs and regional restrictions. ${PRO}`
|
|
280
|
+
),
|
|
252
281
|
retry: z.number().int().nonnegative().optional(),
|
|
253
282
|
scripts: stringOrStringArraySchema.optional(),
|
|
254
283
|
scroll: z.string().min(1).optional(),
|
|
255
|
-
staleTtl: z
|
|
284
|
+
staleTtl: z
|
|
285
|
+
.union([z.string(), z.number(), booleanSchema])
|
|
286
|
+
.optional()
|
|
287
|
+
.describe(
|
|
288
|
+
`Serve stale cached content while refreshing it in the background. ${PRO}`
|
|
289
|
+
),
|
|
256
290
|
styles: stringOrStringArraySchema.optional(),
|
|
257
291
|
timeout: stringOrNumberSchema.optional(),
|
|
258
292
|
ttl: stringOrNumberSchema.optional(),
|
|
@@ -386,8 +420,19 @@ export const lighthouseInputSchema = baseSchema
|
|
|
386
420
|
|
|
387
421
|
export const searchInputSchema = z
|
|
388
422
|
.object({
|
|
389
|
-
query: z
|
|
390
|
-
|
|
423
|
+
query: z
|
|
424
|
+
.string()
|
|
425
|
+
.min(1)
|
|
426
|
+
.describe(
|
|
427
|
+
'Google search query. Operators like site:, filetype: or quotes work as-is.'
|
|
428
|
+
),
|
|
429
|
+
apiKey: z
|
|
430
|
+
.string()
|
|
431
|
+
.min(1)
|
|
432
|
+
.optional()
|
|
433
|
+
.describe(
|
|
434
|
+
'Microlink API key. Required for this tool: Google search runs on the PRO endpoint.'
|
|
435
|
+
),
|
|
391
436
|
type: z
|
|
392
437
|
.enum([
|
|
393
438
|
'search',
|
package/src/tools/register.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
client,
|
|
5
5
|
resolveApiKey
|
|
6
6
|
} from '../microlink-client.js'
|
|
7
|
+
import { outputSchemas } from '../output-schemas.js'
|
|
7
8
|
|
|
8
9
|
function getHeaderValueCaseInsensitive (headers, headerName) {
|
|
9
10
|
if (!headers || typeof headers !== 'object') {
|
|
@@ -79,9 +80,16 @@ export function register (
|
|
|
79
80
|
invoke,
|
|
80
81
|
annotations = READ_ONLY_ANNOTATIONS
|
|
81
82
|
) {
|
|
83
|
+
// Every tool wraps its result as `structuredContent.data`; the output
|
|
84
|
+
// schema describes that `data` value (see output-schemas.js). Error
|
|
85
|
+
// results are exempt: the SDK skips output validation when `isError`.
|
|
86
|
+
const key = name.replace(/^microlink_/, '')
|
|
87
|
+
const dataSchema = outputSchemas[key]
|
|
88
|
+
const outputSchema = dataSchema ? { data: dataSchema } : undefined
|
|
89
|
+
|
|
82
90
|
server.registerTool(
|
|
83
91
|
name,
|
|
84
|
-
{ description, inputSchema, annotations },
|
|
92
|
+
{ description, inputSchema, outputSchema, annotations },
|
|
85
93
|
async (args, extra) => {
|
|
86
94
|
const parsed = inputSchema.safeParse(args)
|
|
87
95
|
|