getdr-mcp 0.1.0 → 0.2.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/CHANGELOG.md CHANGED
@@ -6,6 +6,34 @@ and the project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.0] - 2026-10-29
10
+
11
+ ### Added
12
+
13
+ - Records of invalid targets carry `"error":"invalid target"` (also in
14
+ `outputSchema`). Through a getdr server, a target its Ahrefs refuses is no
15
+ longer reported as "no response".
16
+ - The release workflow publishes `server.json` to the MCP Registry.
17
+
18
+ ### Fixed
19
+
20
+ - Through the binary, a target starting with `#` was skipped as a comment of
21
+ the `-f -` list; such lists now go as arguments.
22
+
23
+ ### Changed
24
+
25
+ - The binary backend needs getdr 0.2.0 or newer (for the `error` field).
26
+ - Without `version`, the getdr server's or binary's config decides; before,
27
+ `dr3` was always sent.
28
+ - The text result is only the JSON array: the note after it about targets
29
+ without a response is gone (their `checked_at` is `null`). An error for
30
+ "no response for any target" quotes getdr instead of guessing a cause.
31
+
32
+ ### Removed
33
+
34
+ - The Ahrefs attribution line at the end of the `get_dr` text result and the
35
+ `attribution` field of `structuredContent` (and of `outputSchema`).
36
+
9
37
  ## [0.1.0] - 2026-10-28
10
38
 
11
39
  ### Added
@@ -26,5 +54,6 @@ and the project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
26
54
  - Release workflow with npm trusted publishing, and `npm run release` with a
27
55
  version and tarball check for manual publishes.
28
56
 
29
- [Unreleased]: https://github.com/index-pages/getdr-mcp/compare/v0.1.0...HEAD
57
+ [Unreleased]: https://github.com/index-pages/getdr-mcp/compare/v0.2.0...HEAD
58
+ [0.2.0]: https://github.com/index-pages/getdr-mcp/compare/v0.1.0...v0.2.0
30
59
  [0.1.0]: https://github.com/index-pages/getdr-mcp/releases/tag/v0.1.0
package/README.md CHANGED
@@ -47,8 +47,10 @@ Checked on every call, in this order:
47
47
  1. `GETDR_API` is set – one `POST $GETDR_API/dr` to that getdr server (`getdr serve`), with
48
48
  `Authorization: Bearer $GETDR_SERVE_TOKEN` when the token is set. The server holds the Ahrefs key and the
49
49
  shared cache, so this machine needs neither.
50
- 2. `getdr` is on `PATH` – runs `getdr --format json -q --version V [--fresh] -f -` with the targets on
51
- stdin, no shell. The binary uses its own config, cache and key (`AHREFS_KEY`), or its own `api` setting.
50
+ 2. `getdr` (0.2.0 or newer) is on `PATH` – runs `getdr --format json -q [--version V] [--fresh] -f -` with
51
+ the targets on stdin, no shell (as arguments after `--` when a target starts with `#`, which `-f` would
52
+ skip as a comment). The binary uses its own config, cache and key (`AHREFS_KEY`), or its own `api`
53
+ setting.
52
54
  3. Neither – the tool returns an error with setup instructions.
53
55
 
54
56
  Install the binary with `go install github.com/index-pages/getdr@latest` or from
@@ -68,7 +70,7 @@ support). The binary backend honours `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY`
68
70
  | argument | type | |
69
71
  |---|---|---|
70
72
  | `targets` | string[] | required, non-empty; domains or URLs |
71
- | `version` | `"dr3"` or `"dr4"` | DR formula, default `dr3` (always sent explicitly, so a server or config set to `dr4` does not change it); values are not comparable between versions |
73
+ | `version` | `"dr3"` or `"dr4"` | DR formula; when left out, the backend's config decides: the getdr server's with `GETDR_API`, otherwise the binary's (also when that binary uses a server), `dr3` unless set; values are not comparable between versions |
72
74
  | `fresh` | boolean | ignore the cache and fetch again |
73
75
 
74
76
  Targets are normalized by getdr: a URL becomes its host, `www.` is dropped, everything is lower-cased and
@@ -76,26 +78,25 @@ duplicates are removed. Results follow the input order.
76
78
 
77
79
  ## Output
78
80
 
79
- The text result is a JSON array with one record per target, then the attribution line:
81
+ The text result is a JSON array with one record per target, and nothing else:
80
82
 
81
83
  ```
82
84
  [
83
85
  {"target":"ahrefs.com","dr":91,"version":"dr3","checked_at":"2026-10-28T12:00:00Z","cached":true},
84
86
  {"target":"nodata.example","dr":null,"version":"dr3","checked_at":"2026-10-28T12:00:00Z","cached":false}
85
87
  ]
86
-
87
- Domain Rating by Ahrefs – https://ahrefs.com/
88
88
  ```
89
89
 
90
90
  The record is the getdr JSON contract, the same as `getdr --format json` and `getdr serve`. `dr` is `null`
91
- when Ahrefs has no data; `checked_at` is `null` too when there was no response (timeout, rate limit). Clients on
92
- MCP protocol 2025-06-18 or later also get `structuredContent` – `{"results": […], "attribution": "…"}` – and an
93
- `outputSchema` for it.
91
+ when Ahrefs has no data; `checked_at` is `null` too when there was no response or the target is invalid. An
92
+ invalid target (not a domain or URL, or refused by Ahrefs, such as `localhost`) also has
93
+ `"error":"invalid target"`; no other record has an `error` key. Clients on MCP protocol 2025-06-18 or later
94
+ also get `structuredContent` – `{"results": […]}` – and an `outputSchema` for it.
94
95
 
95
- When some targets got no response, the result is a normal one: all records, followed by a note naming those
96
- targets. Everything else comes back as a tool error (`isError: true`) with getdr's message:
96
+ Invalid targets and targets without a response are part of a normal result. Failures come back as a tool
97
+ error (`isError: true`) with getdr's message:
97
98
 
98
- - no target got a response – an error with the reason;
99
+ - no target got a response at all;
99
100
  - getdr server answered 401 – the token is missing or wrong, set `GETDR_SERVE_TOKEN`;
100
101
  - getdr server answered 503, or the binary exited with code 3 – the Ahrefs key is missing or rejected;
101
102
  - the server is unreachable, the binary failed, or a call timed out.
@@ -107,7 +108,8 @@ handshake) and the `initialize`-based 2025-11-25, 2025-06-18, 2025-03-26 and 202
107
108
 
108
109
  Domain Rating data comes from [Ahrefs](https://ahrefs.com/) and is subject to the
109
110
  [Domain Rating License](https://ahrefs.com/legal/domain-rating-license), which requires the attribution
110
- "Domain Rating by Ahrefs". Every result ends with it. Keep it when you publish the numbers.
111
+ "Domain Rating by Ahrefs" wherever you publish the numbers. The tool result carries only the data, with
112
+ no attribution in it.
111
113
 
112
114
  getdr is not affiliated with Ahrefs.
113
115
 
@@ -145,10 +147,11 @@ For any manual publish use `npm run release` (build, tests, `check-release`, `np
145
147
  `npm publish`: with install scripts disabled (`ignore-scripts`) `prepack` does not run and the package would
146
148
  ship without `dist/`. `prepack` stays as a fallback for setups that run scripts.
147
149
 
148
- MCP Registry (manual, after the npm release): install `mcp-publisher` from the
149
- [registry releases](https://github.com/modelcontextprotocol/registry/releases), run
150
- `mcp-publisher login github` as a member of the `index-pages` GitHub org, then `mcp-publisher publish` in
151
- the repo root. It reads `server.json` and checks `mcpName` in the published npm package.
150
+ The same workflow then publishes `server.json` to the [MCP Registry](https://registry.modelcontextprotocol.io)
151
+ as `io.github.index-pages/getdr-mcp`: it waits until npm shows the new version (the registry checks `mcpName`
152
+ in the npm package), proves the `index-pages` namespace with the workflow's OIDC token
153
+ (`mcp-publisher login github-oidc`, no secret) and runs `mcp-publisher publish` with a pinned, checksummed
154
+ `mcp-publisher`. A version already in the registry is skipped.
152
155
 
153
156
  ## License
154
157
 
package/dist/backends.js CHANGED
@@ -13,7 +13,7 @@ export class BackendError extends Error {
13
13
  export const TIMEOUT_MS = 120_000;
14
14
  const MAX_DETAIL = 2000;
15
15
  export function isNoResponse(r) {
16
- return r.dr === null && r.checked_at === null;
16
+ return r.dr === null && r.checked_at === null && r.error === undefined;
17
17
  }
18
18
  export function toRecord(v) {
19
19
  const o = v;
@@ -23,10 +23,14 @@ export function toRecord(v) {
23
23
  !(o.dr === null || typeof o.dr === "number") ||
24
24
  typeof o.version !== "string" ||
25
25
  !(o.checked_at === null || typeof o.checked_at === "string") ||
26
- typeof o.cached !== "boolean") {
26
+ typeof o.cached !== "boolean" ||
27
+ !(o.error === undefined || typeof o.error === "string")) {
27
28
  throw new Error(`unexpected record ${JSON.stringify(v)?.slice(0, 200)}`);
28
29
  }
29
- return { target: o.target, dr: o.dr, version: o.version, checked_at: o.checked_at, cached: o.cached };
30
+ const r = { target: o.target, dr: o.dr, version: o.version, checked_at: o.checked_at, cached: o.cached };
31
+ if (o.error !== undefined)
32
+ r.error = o.error;
33
+ return r;
30
34
  }
31
35
  function clip(s) {
32
36
  s = s.trim();
@@ -47,8 +51,10 @@ function serverError(body) {
47
51
  export async function viaApi(base, token, q) {
48
52
  const url = base.replace(/\/+$/, "") + "/dr";
49
53
  const where = `getdr server ${base} (GETDR_API)`;
50
- // Always explicit, so a server configured for dr4 still answers dr3 by default.
51
- const body = { targets: q.targets, version: q.version ?? "dr3" };
54
+ // Without a version the server's own default applies.
55
+ const body = { targets: q.targets };
56
+ if (q.version)
57
+ body.version = q.version;
52
58
  if (q.fresh)
53
59
  body.fresh = true;
54
60
  const headers = {
@@ -93,8 +99,7 @@ export async function viaApi(base, token, q) {
93
99
  if (!Array.isArray(parsed.results))
94
100
  throw new Error('no "results" array');
95
101
  const records = parsed.results.map(toRecord);
96
- const detail = records.some(isNoResponse) ? "the getdr server got no answer from Ahrefs (timeout or rate limit)" : "";
97
- return { records, detail };
102
+ return { records, detail: records.some(isNoResponse) ? "the getdr server got no answer from Ahrefs" : "" };
98
103
  }
99
104
  catch (err) {
100
105
  throw new BackendError(`${where} sent an unexpected response: ${err.message}`);
@@ -131,12 +136,18 @@ export function findBinary(env) {
131
136
  }
132
137
  export function viaBinary(bin, env, q) {
133
138
  // Targets go through stdin (-f -): no shell, no argv length limits, and a
134
- // target that starts with "-" is never read as a flag.
139
+ // target that starts with "-" is never read as a flag. -f skips lines
140
+ // starting with "#", so such a list goes as arguments after "--" instead.
135
141
  const argv = ["--format", "json", "-q"];
136
- argv.push("--version", q.version ?? "dr3"); // explicit: the binary's own config may say dr4
142
+ if (q.version)
143
+ argv.push("--version", q.version);
137
144
  if (q.fresh)
138
145
  argv.push("--fresh");
139
- argv.push("-f", "-");
146
+ const asArgs = q.targets.some((t) => t.trim().startsWith("#"));
147
+ if (asArgs)
148
+ argv.push("--", ...q.targets);
149
+ else
150
+ argv.push("-f", "-");
140
151
  return new Promise((resolve, reject) => {
141
152
  const fail = (msg) => reject(new BackendError(msg));
142
153
  let child;
@@ -192,13 +203,14 @@ export function viaBinary(bin, env, q) {
192
203
  return;
193
204
  }
194
205
  // Exit 2 without records is an error (bad flag, cache, no valid
195
- // targets); with records it means some targets got no response.
206
+ // targets); with records it means some targets were invalid or got no
207
+ // response.
196
208
  if (code === 2 && records.length === 0) {
197
209
  fail(stderr || "getdr failed (exit 2)");
198
210
  return;
199
211
  }
200
212
  resolve({ records, detail: code === 2 ? stderr : "" });
201
213
  });
202
- child.stdin.end(q.targets.join("\n") + "\n");
214
+ child.stdin.end(asArgs ? "" : q.targets.join("\n") + "\n");
203
215
  });
204
216
  }
package/dist/tool.js CHANGED
@@ -1,7 +1,6 @@
1
1
  // The get_dr tool: argument checks, backend choice and the result shape.
2
2
  import { BackendError, findBinary, isNoResponse, viaApi, viaBinary } from "./backends.js";
3
3
  import { HOMEPAGE } from "./version.js";
4
- export const ATTRIBUTION = "Domain Rating by Ahrefs – https://ahrefs.com/";
5
4
  const DESCRIPTION = "Ahrefs Domain Rating (0–100) for domains or URLs, via getdr by Index Pages";
6
5
  export function toolDefinition(structured) {
7
6
  const tool = {
@@ -20,7 +19,7 @@ export function toolDefinition(structured) {
20
19
  version: {
21
20
  type: "string",
22
21
  enum: ["dr3", "dr4"],
23
- description: "DR formula: dr3 or dr4 (the new one); default dr3, whatever the getdr config says. Values are not comparable between versions.",
22
+ description: "DR formula: dr3 or dr4 (the new one); default: the getdr server's or binary's config, dr3 unless set there. Values are not comparable between versions.",
24
23
  },
25
24
  fresh: {
26
25
  type: "boolean",
@@ -42,17 +41,17 @@ export function toolDefinition(structured) {
42
41
  type: "object",
43
42
  properties: {
44
43
  target: { type: "string" },
45
- dr: { type: ["number", "null"], description: "null: no data, or no response when checked_at is null" },
44
+ dr: { type: ["number", "null"], description: "null: no data, or no response or an invalid target when checked_at is null" },
46
45
  version: { type: "string", enum: ["dr3", "dr4"] },
47
- checked_at: { type: ["string", "null"], description: "RFC 3339 UTC; null when there was no response" },
46
+ checked_at: { type: ["string", "null"], description: "RFC 3339 UTC; null for no response or an invalid target" },
48
47
  cached: { type: "boolean" },
48
+ error: { type: "string", enum: ["invalid target"], description: "only on a target that is not a domain or URL" },
49
49
  },
50
50
  required: ["target", "dr", "version", "checked_at", "cached"],
51
51
  },
52
52
  },
53
- attribution: { type: "string" },
54
53
  },
55
- required: ["results", "attribution"],
54
+ required: ["results"],
56
55
  };
57
56
  }
58
57
  return tool;
@@ -114,19 +113,13 @@ export async function callGetDr(raw, opts) {
114
113
  return toolError(err.message);
115
114
  throw err;
116
115
  }
117
- const missing = records.filter(isNoResponse);
118
- if (records.length > 0 && missing.length === records.length) {
119
- return toolError(`No response for any target${detail ? `: ${detail}` : " (timeout or rate limit on the Ahrefs side)"}. Try again later.`);
116
+ // Nothing answered at all is a failure of the backend, not a result.
117
+ if (records.length > 0 && records.every(isNoResponse)) {
118
+ return toolError(detail ? `No response for any target: ${detail}` : "No response for any target.");
120
119
  }
121
- let text = records.length ? "[\n" + records.map((r) => JSON.stringify(r)).join(",\n") + "\n]\n" : "[]\n";
122
- if (missing.length > 0) {
123
- text +=
124
- `\nNo response for ${missing.length} of ${records.length} target(s), dr and checked_at are null: ` +
125
- `${missing.map((r) => r.target).join(", ")}. Try these again later.\n`;
126
- }
127
- text += `\n${ATTRIBUTION}`;
120
+ const text = records.length ? "[\n" + records.map((r) => JSON.stringify(r)).join(",\n") + "\n]\n" : "[]\n";
128
121
  const result = { content: [{ type: "text", text }] };
129
122
  if (opts.structured)
130
- result.structuredContent = { results: records, attribution: ATTRIBUTION };
123
+ result.structuredContent = { results: records };
131
124
  return result;
132
125
  }
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // Kept in sync with package.json (a test checks it).
2
- export const VERSION = "0.1.0";
2
+ export const VERSION = "0.2.0";
3
3
  export const HOMEPAGE = "https://index-pages.com/getdr";
4
4
  export const USER_AGENT = `getdr-mcp/${VERSION} (+${HOMEPAGE})`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getdr-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server for Ahrefs Domain Rating via getdr by Index Pages",
5
5
  "keywords": [
6
6
  "mcp",