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 +30 -1
- package/README.md +20 -17
- package/dist/backends.js +24 -12
- package/dist/tool.js +10 -17
- package/dist/version.js +1 -1
- package/package.json +1 -1
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.
|
|
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
|
|
51
|
-
stdin, no shell
|
|
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,
|
|
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,
|
|
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
|
|
92
|
-
|
|
93
|
-
`
|
|
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
|
-
|
|
96
|
-
|
|
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
|
|
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"
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
51
|
-
const body = { targets: q.targets
|
|
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
|
-
|
|
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
|
-
|
|
142
|
+
if (q.version)
|
|
143
|
+
argv.push("--version", q.version);
|
|
137
144
|
if (q.fresh)
|
|
138
145
|
argv.push("--fresh");
|
|
139
|
-
|
|
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
|
|
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
|
|
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
|
|
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"
|
|
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
|
-
|
|
118
|
-
if (records.length > 0 &&
|
|
119
|
-
return toolError(`No response for any target
|
|
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
|
-
|
|
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
|
|
123
|
+
result.structuredContent = { results: records };
|
|
131
124
|
return result;
|
|
132
125
|
}
|
package/dist/version.js
CHANGED