opedd-mcp 0.12.7 → 0.12.9
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 +4 -3
- package/dist/index.js +71 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Then add it to your MCP host (see [Claude Desktop / Cursor / Windsurf setup](#cl
|
|
|
35
35
|
|
|
36
36
|
## Hosted endpoint — no install (mcp.opedd.com)
|
|
37
37
|
|
|
38
|
-
Prefer zero-install? The same
|
|
38
|
+
Prefer zero-install? The same 21 tools are served hosted at **`https://mcp.opedd.com/mcp`** (Streamable HTTP). Auth is an `Authorization: Bearer` header with any Opedd key — public discovery tools need no auth at all.
|
|
39
39
|
|
|
40
40
|
**Claude Messages API** (production agents):
|
|
41
41
|
|
|
@@ -82,7 +82,7 @@ Key routing: `opedd_sk_*` (legacy `opedd_pub_*`) → publisher tools · `opedd_b
|
|
|
82
82
|
|
|
83
83
|
## What it does
|
|
84
84
|
|
|
85
|
-
Exposes up to
|
|
85
|
+
Exposes up to 21 tools to any AI assistant (some are conditional on env vars):
|
|
86
86
|
|
|
87
87
|
Every tool carries a `title` and MCP annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) so an assistant can tell which tools only read and which spend money or write (v0.12.0).
|
|
88
88
|
|
|
@@ -92,7 +92,8 @@ Every tool carries a `title` and MCP annotations (`readOnlyHint`, `destructiveHi
|
|
|
92
92
|
|------|-------------|
|
|
93
93
|
| `search_content` | **Free discovery.** Full-text search across everything licensable (v0.8.0) — ranked matches with publisher, prices, word count and a description snippet; quotes for phrases, `-word` to exclude. Returns **no article text** — use `search_passages` to actually read |
|
|
94
94
|
| `search_passages` | **Paid per answer, or included in a monthly subscription.** Ask a question, get licensed passages that answer it, with citations. Per-question licences: charged once per publisher per question at that publisher's own price, capped at 300 words or 25% of the article, counted cumulatively. Monthly AI answers subscriptions (v0.12.0): included, not capped, from the subscription's start. Narrow with `publishers`, `published_after`, `published_before`, `max_results` (1–25). Needs a buyer key with the `search` scope. Re-asking within 24h is free |
|
|
95
|
-
| `lookup_content` | Look up an article by URL — returns title, publisher, pricing |
|
|
95
|
+
| `lookup_content` | Look up an article by URL — returns title, publisher, pricing, or that it is not on Opedd yet |
|
|
96
|
+
| `request_publication` | Ask to be told when a publication Opedd does not carry yet becomes available (url + email; no key) |
|
|
96
97
|
| `purchase_license` | Buy a Human republication licence for one article via Stripe — returns OP-XXXX-XXXX key. Requires `terms_accepted: true` (genuine buyer assent to opedd.com/terms; recorded with the licence) |
|
|
97
98
|
| `verify_license` | Verify a license key — returns validity, article, publisher, public-record proof |
|
|
98
99
|
| `browse_registry` | Browse the public Opedd registry — lists issued LICENSES (use `publisher_directory` to browse publishers themselves) |
|
package/dist/index.js
CHANGED
|
@@ -53,6 +53,7 @@ async function opeddFetch(creds, path, options = {}) {
|
|
|
53
53
|
// proxy must surface as "HTTP 502", not a JSON SyntaxError.
|
|
54
54
|
if (!res.ok) {
|
|
55
55
|
let msg = `HTTP ${res.status}`;
|
|
56
|
+
let details;
|
|
56
57
|
try {
|
|
57
58
|
const errBody = await res.json();
|
|
58
59
|
// Backend envelope upgraded to structured errors ({ error: { code,
|
|
@@ -60,6 +61,8 @@ async function opeddFetch(creds, path, options = {}) {
|
|
|
60
61
|
// object into Error() rendered "[object Object]" — every agent-facing
|
|
61
62
|
// failure was unreadable (launch-gaps follow-on, 2026-07-17).
|
|
62
63
|
const e = errBody?.error;
|
|
64
|
+
if (typeof e === "object" && e !== null)
|
|
65
|
+
details = e.details;
|
|
63
66
|
msg =
|
|
64
67
|
(typeof e === "object" && e !== null ? [e.code, e.message].filter(Boolean).join(": ") : e) ||
|
|
65
68
|
errBody?.message ||
|
|
@@ -68,7 +71,10 @@ async function opeddFetch(creds, path, options = {}) {
|
|
|
68
71
|
catch {
|
|
69
72
|
// non-JSON error body — keep the HTTP-status default
|
|
70
73
|
}
|
|
71
|
-
|
|
74
|
+
// The status rides on the error so a caller can treat one status as an
|
|
75
|
+
// answer (lookup_content: 404 = not on Opedd yet) without parsing text;
|
|
76
|
+
// error.details rides too (lookup_content: the publication at that site).
|
|
77
|
+
throw Object.assign(new Error(msg), { status: res.status, details });
|
|
72
78
|
}
|
|
73
79
|
return await res.json();
|
|
74
80
|
}
|
|
@@ -134,7 +140,7 @@ function err(msg) {
|
|
|
134
140
|
// Only the refusal text changed: what's missing, how to supply it on BOTH
|
|
135
141
|
// channels (env var locally, Authorization header on the hosted gateway),
|
|
136
142
|
// where credentials come from, and which tools need none.
|
|
137
|
-
const NO_AUTH_TOOLS = "lookup_content, publisher_directory, verify_license, browse_registry, rsl_get, detect_platform";
|
|
143
|
+
const NO_AUTH_TOOLS = "lookup_content, publisher_directory, verify_license, browse_registry, rsl_get, detect_platform, request_publication";
|
|
138
144
|
function credErr(missing, envVar, howToGet) {
|
|
139
145
|
return err(`${missing} — none was provided. ` +
|
|
140
146
|
`Supply it as the ${envVar} env var (local/stdio install) or as an "Authorization: Bearer <value>" header (hosted gateway, https://mcp.opedd.com/mcp). ` +
|
|
@@ -182,7 +188,7 @@ function buildTools(has) {
|
|
|
182
188
|
const TOOLS = [
|
|
183
189
|
{
|
|
184
190
|
name: "lookup_content",
|
|
185
|
-
description: "Looks up an article on Opedd by its web address. Returns the title, publication, available licence types and prices, or that
|
|
191
|
+
description: "Looks up an article on Opedd by its web address. Returns the title, publication, available licence types and prices; for a publication's homepage, the publication and its licensing page; or that it is not on Opedd yet (then request_publication records the request).",
|
|
186
192
|
inputSchema: {
|
|
187
193
|
type: "object",
|
|
188
194
|
required: ["url"],
|
|
@@ -194,6 +200,21 @@ function buildTools(has) {
|
|
|
194
200
|
},
|
|
195
201
|
},
|
|
196
202
|
},
|
|
203
|
+
{
|
|
204
|
+
name: "request_publication",
|
|
205
|
+
description: "Asks Opedd to tell you when a publication it does not carry yet becomes available to license. Use it after lookup_content says an article is not on Opedd. Records the request against the email given; nothing is bought. If the publication is already on Opedd, returns its licensing page instead. Free; no key needed.",
|
|
206
|
+
inputSchema: {
|
|
207
|
+
type: "object",
|
|
208
|
+
required: ["url", "email"],
|
|
209
|
+
properties: {
|
|
210
|
+
url: { type: "string", description: "The web address of the publication or of one of its articles." },
|
|
211
|
+
email: { type: "string", description: "Where Opedd should write when the publication is available (the user's email, with their consent)." },
|
|
212
|
+
name: { type: "string", description: "Optional: the requester's name." },
|
|
213
|
+
organization: { type: "string", description: "Optional: the requester's company or fund." },
|
|
214
|
+
note: { type: "string", description: "Optional: what the content is needed for (max 1000 characters)." },
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
},
|
|
197
218
|
{
|
|
198
219
|
name: "search_content",
|
|
199
220
|
description: "Full-text search of the articles licensable on Opedd. Returns matching articles with the publication, prices, word count and a short description; never article text. Supports quoted phrases and -word to exclude a word. Free; no key needed.",
|
|
@@ -760,6 +781,8 @@ function buildTools(has) {
|
|
|
760
781
|
const read = (title) => ({ title, readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false });
|
|
761
782
|
export const TOOL_ANNOTATIONS = {
|
|
762
783
|
lookup_content: read("Look up an article's licence and price"),
|
|
784
|
+
// Writes one request row (a repeat for the same email writes nothing): not read-only, idempotent.
|
|
785
|
+
request_publication: { title: "Ask to be told when a publication is available", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
763
786
|
search_content: read("Find who covers a topic (free, no text)"),
|
|
764
787
|
purchase_license: { title: "Buy an article licence", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
765
788
|
verify_license: read("Verify a licence key"),
|
|
@@ -850,8 +873,51 @@ export async function dispatchTool(name, args, creds = ENV_CREDS) {
|
|
|
850
873
|
const { url } = args;
|
|
851
874
|
if (!url)
|
|
852
875
|
return err("url is required");
|
|
853
|
-
|
|
854
|
-
|
|
876
|
+
try {
|
|
877
|
+
const data = await opeddFetch(creds, `/lookup-article?url=${encodeURIComponent(url)}`);
|
|
878
|
+
return ok(data);
|
|
879
|
+
}
|
|
880
|
+
catch (e) {
|
|
881
|
+
// A miss is an answer, not a failure (2026-10-22). Directories score
|
|
882
|
+
// servers on error rates, and the honest reply is "not on Opedd yet"
|
|
883
|
+
// with a way to ask for it, never "coming soon".
|
|
884
|
+
if (e.status === 404) {
|
|
885
|
+
// No article at this address, but the address is a publication on
|
|
886
|
+
// Opedd (its homepage, or a piece not imported yet): say so, and
|
|
887
|
+
// where to license it (2026-10-24). Agents that looked up a
|
|
888
|
+
// publication's homepage were told it was not on Opedd.
|
|
889
|
+
const details = e.details;
|
|
890
|
+
if (details?.on_opedd === true && details.publication) {
|
|
891
|
+
return ok({
|
|
892
|
+
on_opedd: true,
|
|
893
|
+
url,
|
|
894
|
+
publication: details.publication,
|
|
895
|
+
message: `There is no single article at this address, but ${details.publication.name ?? "this publication"} is on Opedd. ` +
|
|
896
|
+
"Its licences and prices are on its licensing page; use search_content or search_passages to find its articles.",
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
return ok({
|
|
900
|
+
on_opedd: false,
|
|
901
|
+
url,
|
|
902
|
+
message: "This article is not on Opedd yet. If you want to license this publication, call request_publication " +
|
|
903
|
+
"with this url and an email, and Opedd will write when it becomes available.",
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
throw e;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
// ── request_publication ────────────────────────────────────────────────
|
|
910
|
+
case "request_publication": {
|
|
911
|
+
const { url, email, name, organization, note } = args;
|
|
912
|
+
if (!url)
|
|
913
|
+
return err("url is required");
|
|
914
|
+
if (!email)
|
|
915
|
+
return err("email is required: Opedd writes to it when the publication is available");
|
|
916
|
+
const envelope = (await opeddFetch(creds, "/publication-request", {
|
|
917
|
+
method: "POST",
|
|
918
|
+
body: JSON.stringify({ url, email, name, organization, note }),
|
|
919
|
+
}));
|
|
920
|
+
return ok(envelope.data ?? envelope);
|
|
855
921
|
}
|
|
856
922
|
// ── search_content ─────────────────────────────────────────────────────
|
|
857
923
|
case "search_content": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opedd-mcp",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.9",
|
|
4
4
|
"description": "Content licensing for the AI era. Licensed, rights-cleared content for AI agents, with a verifiable license key for every article. The MCP alternative to unlicensed scraping for RAG and AI search.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|