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 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 20 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.
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 20 tools to any AI assistant (some are conditional on env vars):
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
- throw new Error(msg);
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 the article is not on Opedd.",
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
- const data = await opeddFetch(creds, `/lookup-article?url=${encodeURIComponent(url)}`);
854
- return ok(data);
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.7",
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",