@microlink/mcp 2.3.0 → 2.4.1

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 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? } }`. A `429` also includes a free-quota `hint`.
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 (`50 requests/day`) and upgrade/API key guidance at [microlink.io/#pricing](https://microlink.io/#pricing).
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.3.0",
5
+ "version": "2.4.1",
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": "9e479486674289cd4f5257b4c1fd5ad527f6ebb1"
69
+ "gitHead": "f2d20e6f64995a7084ed097d5441d0f0f4950c76"
70
70
  }
@@ -1,7 +1,25 @@
1
1
  import createClient, { MicrolinkError } from 'microlink.io'
2
2
 
3
- const FREE_QUOTA_EXCEEDED_HINT =
4
- 'Free daily quota reached (50 requests/day). Extend your limit by getting an API key at https://microlink.io/#pricing.'
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()
@@ -14,24 +32,52 @@ export function resolveApiKey (inputApiKey, headerApiKey) {
14
32
  )
15
33
  }
16
34
 
35
+ const isPlainObject = value =>
36
+ value !== null && typeof value === 'object' && !Array.isArray(value)
37
+
38
+ function toToolResponse (isError, field, value) {
39
+ return {
40
+ isError,
41
+ structuredContent: { [field]: value },
42
+ content: [{ type: 'text', text: JSON.stringify(value, null, 2) }]
43
+ }
44
+ }
45
+
17
46
  // Every tool returns the library's direct result (a string, array, or object).
18
47
  // MCP `structuredContent` must be an object, so wrap the value under `data`.
19
48
  export function asToolResult (value) {
20
- const data = value ?? null
21
- return {
22
- isError: false,
23
- structuredContent: { data },
24
- content: [{ type: 'text', text: JSON.stringify(data, null, 2) }]
25
- }
49
+ return toToolResponse(false, 'data', value ?? null)
26
50
  }
27
51
 
28
- export function asErrorResult (error) {
52
+ // The API wraps some failures in a generic top-level message ("The request
53
+ // has been not processed…"), while the specific cause travels in `data`
54
+ // (e.g. `data.url`). Only then should the data-derived message lead: a
55
+ // specific description is the real cause and must not be hidden by
56
+ // auxiliary strings in `data`.
57
+ const GENERIC_API_MESSAGE = 'The request has been not processed.'
58
+
59
+ function toErrorPayload (error) {
29
60
  const isMql = error instanceof MicrolinkError
30
61
  const statusCode = isMql ? error.statusCode : undefined
62
+ const description = isMql ? error.description : undefined
63
+
64
+ const details = isMql && isPlainObject(error.data) ? error.data : undefined
65
+ const detailMessage =
66
+ details &&
67
+ Object.values(details)
68
+ .filter(value => typeof value === 'string')
69
+ .join(' ')
70
+
71
+ const isGenericDescription =
72
+ typeof description === 'string' &&
73
+ description.startsWith(GENERIC_API_MESSAGE)
31
74
 
32
75
  const payload = {
33
76
  message:
34
- (isMql ? error.description : undefined) || error?.message || String(error)
77
+ (isGenericDescription ? detailMessage : undefined) ||
78
+ description ||
79
+ error?.message ||
80
+ String(error)
35
81
  }
36
82
 
37
83
  if (isMql) {
@@ -40,13 +86,32 @@ export function asErrorResult (error) {
40
86
  if (statusCode) payload.statusCode = statusCode
41
87
  if (error.url) payload.url = error.url
42
88
  if (error.more) payload.more = error.more
89
+ if (details) payload.details = details
43
90
  }
44
91
 
45
- if (statusCode === 429) payload.hint = FREE_QUOTA_EXCEEDED_HINT
92
+ const guidance = isMql && ERROR_GUIDANCE[error.code]
93
+ if (guidance) {
94
+ payload.reason = guidance.reason
95
+ payload.capability = guidance.capability
96
+ payload.hint = guidance.hint
97
+ payload.upgrade = { plan: 'pro', url: UPGRADE_URL }
98
+ }
46
99
 
47
- return {
48
- isError: true,
49
- structuredContent: { error: payload },
50
- content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }]
100
+ if (statusCode === 429) {
101
+ payload.reason = 'quota_exceeded'
102
+ payload.hint = FREE_QUOTA_EXCEEDED_HINT
51
103
  }
104
+
105
+ return payload
106
+ }
107
+
108
+ export function asErrorResult (error) {
109
+ const payload =
110
+ isPlainObject(error) &&
111
+ !(error instanceof Error) &&
112
+ typeof error.message === 'string'
113
+ ? error
114
+ : toErrorPayload(error)
115
+
116
+ return toToolResponse(true, 'error', payload)
52
117
  }
package/src/schemas.js CHANGED
@@ -214,6 +214,9 @@ export const metaConfigSchema = objectLikeSchema(
214
214
  .strict()
215
215
  )
216
216
 
217
+ const optionalApiKey = description =>
218
+ z.string().min(1).optional().describe(description)
219
+
217
220
  const baseSchema = z.object({
218
221
  url: z
219
222
  .string()
@@ -221,13 +224,9 @@ const baseSchema = z.object({
221
224
  .describe(
222
225
  'Public URL of the page to process. Include the protocol, for example https://example.com.'
223
226
  ),
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
- )
227
+ apiKey: optionalApiKey(
228
+ 'Microlink PRO API key. Omit it unless you have one: requests then use the MICROLINK_API_KEY environment variable or the free endpoint.'
229
+ )
231
230
  })
232
231
 
233
232
  const fullShape = {
@@ -242,28 +241,51 @@ const fullShape = {
242
241
  // Shared Microlink API query parameters (see microlink.io/docs/api/parameters).
243
242
  // Product tools layer their own fields on top; these apply to any URL fetch.
244
243
  // `data` is separate: content/collection helpers overwrite it with their field rule.
244
+ // PRO parameters per the Microlink API spec (https://microlink.io/openapi.json):
245
+ // they only take effect with an API key attached to the request.
246
+ const PRO = 'PRO: requires a Microlink API key (Pro plan).'
247
+
245
248
  const browserSchema = {
246
249
  adblock: booleanSchema.optional(),
247
250
  animations: booleanSchema.optional(),
248
- cacheKey: z.string().min(1).optional(),
251
+ cacheKey: z
252
+ .string()
253
+ .min(1)
254
+ .optional()
255
+ .describe(`Custom cache key for the request. ${PRO}`),
249
256
  click: stringOrStringArraySchema.optional(),
250
257
  colorScheme: z.enum(['no-preference', 'light', 'dark']).optional(),
251
258
  device: z.string().min(1).optional(),
252
- filename: z.string().min(1).optional(),
259
+ filename: z
260
+ .string()
261
+ .min(1)
262
+ .optional()
263
+ .describe(`Custom name for the generated asset. ${PRO}`),
253
264
  filter: z.string().min(1).optional(),
254
265
  force: booleanSchema.optional(),
255
266
  headers: objectLikeSchema(
256
267
  z.record(z.string(), z.union([z.string(), z.number(), booleanSchema]))
257
- ).optional(),
268
+ )
269
+ .optional()
270
+ .describe(`Custom HTTP headers sent to the target URL. ${PRO}`),
258
271
  javascript: booleanSchema.optional(),
259
272
  mediaType: z.enum(['screen', 'print']).optional(),
260
273
  modules: stringOrStringArraySchema.optional(),
261
274
  prerender: z.union([z.literal('auto'), booleanSchema]).optional(),
262
- proxy: proxySchema.optional(),
275
+ proxy: proxySchema
276
+ .optional()
277
+ .describe(
278
+ `Proxy rotation to bypass IP rate limits, CAPTCHAs and regional restrictions. ${PRO}`
279
+ ),
263
280
  retry: z.number().int().nonnegative().optional(),
264
281
  scripts: stringOrStringArraySchema.optional(),
265
282
  scroll: z.string().min(1).optional(),
266
- staleTtl: z.union([z.string(), z.number(), booleanSchema]).optional(),
283
+ staleTtl: z
284
+ .union([z.string(), z.number(), booleanSchema])
285
+ .optional()
286
+ .describe(
287
+ `Serve stale cached content while refreshing it in the background. ${PRO}`
288
+ ),
267
289
  styles: stringOrStringArraySchema.optional(),
268
290
  timeout: stringOrNumberSchema.optional(),
269
291
  ttl: stringOrNumberSchema.optional(),
@@ -403,13 +425,9 @@ export const searchInputSchema = z
403
425
  .describe(
404
426
  'Google search query. Operators like site:, filetype: or quotes work as-is.'
405
427
  ),
406
- apiKey: z
407
- .string()
408
- .min(1)
409
- .optional()
410
- .describe(
411
- 'Microlink API key. Required for this tool: Google search runs on the PRO endpoint.'
412
- ),
428
+ apiKey: optionalApiKey(
429
+ 'Microlink API key. Required for this tool: Google search runs on the PRO endpoint.'
430
+ ),
413
431
  type: z
414
432
  .enum([
415
433
  'search',
@@ -94,15 +94,10 @@ export function register (
94
94
  const parsed = inputSchema.safeParse(args)
95
95
 
96
96
  if (!parsed.success) {
97
- const payload = {
97
+ return asErrorResult({
98
98
  message: 'Input validation failed.',
99
99
  issues: parsed.error.issues
100
- }
101
- return {
102
- isError: true,
103
- structuredContent: { error: payload },
104
- content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }]
105
- }
100
+ })
106
101
  }
107
102
 
108
103
  try {