opedd-mcp 0.2.0 → 0.4.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.
@@ -12,7 +12,13 @@ jobs:
12
12
  strategy:
13
13
  fail-fast: false
14
14
  matrix:
15
- node-version: ["18", "20", "22"]
15
+ # Node 18 dropped 2026-05-24 — vitest 4.x (rolldown dep) requires
16
+ # `node:util.styleText` which is only available on Node 21.7+.
17
+ # Node 18 reaches active LTS end April 2025; engines.node in
18
+ # package.json bumped to ">=20" in the same commit. Re-add Node 18
19
+ # only if vitest downgrade ratified (which is the alternative;
20
+ # founder picked engines bump).
21
+ node-version: ["20", "22"]
16
22
  steps:
17
23
  - uses: actions/checkout@v4
18
24
 
@@ -23,11 +29,22 @@ jobs:
23
29
  cache: npm
24
30
 
25
31
  - name: Install dependencies
26
- run: npm ci
32
+ # Using `npm install` rather than `npm ci` because the lockfile
33
+ # generated on macOS doesn't include Linux-conditional optional
34
+ # native deps (e.g. @emnapi/* used by vitest 4.x via napi-rs).
35
+ # `npm install` resolves transitively at install time per-platform;
36
+ # `npm ci` strictly validates lockfile-against-package.json and
37
+ # rejects when platform-specific transitive deps are missing.
38
+ # Trade-off: install is slower + slightly looser; acceptable for
39
+ # this repo's CI scale.
40
+ run: npm install
27
41
 
28
42
  - name: TypeScript compile
29
43
  run: npm run build
30
44
 
45
+ - name: Unit tests (vitest)
46
+ run: npm test
47
+
31
48
  - name: Verify dist/index.js produced
32
49
  run: |
33
50
  test -f dist/index.js || (echo "::error::dist/index.js not produced by tsc"; exit 1)
package/README.md CHANGED
@@ -8,17 +8,20 @@ Lets AI assistants (Claude Desktop, Cursor, Windsurf, or any MCP-compatible host
8
8
 
9
9
  ## What it does
10
10
 
11
- Exposes up to 11 tools to any AI assistant (some are conditional on env vars):
11
+ Exposes up to 16 tools to any AI assistant (some are conditional on env vars):
12
12
 
13
- **Always available — discovery + per-article purchase**
13
+ **Always available — discovery + per-article purchase + onboarding + rights signaling**
14
14
 
15
15
  | Tool | Description |
16
16
  |------|-------------|
17
17
  | `lookup_content` | Look up an article by URL — returns title, publisher, pricing |
18
18
  | `purchase_license` | Buy a single-article license via Stripe — returns OP-XXXX-XXXX key |
19
19
  | `verify_license` | Verify a license key — returns validity, article, publisher, blockchain proof |
20
- | `browse_registry` | Browse the public Opedd registry (global or by publisher) |
20
+ | `browse_registry` | Browse the public Opedd registry — lists issued LICENSES (use `publisher_directory` to browse publishers themselves) |
21
+ | `publisher_directory` | Browse the public Opedd publisher catalog — paginated publishers with article counts + pricing + sample articles (primary buyer-discovery surface for AI labs) |
21
22
  | `purchase_enterprise_license` | Buy a bulk enterprise license covering multiple publishers (Phase 10) — returns Stripe `client_secret` |
23
+ | `rsl_get` | Fetch a publisher's RSL Standard manifest — public discovery surface; `jsonld: true` returns CDSM Article 4(3) signed receipt (Phase 12 W1.1) |
24
+ | `detect_platform` | Detect the content platform behind a URL — returns suggested onboarding workflow for Substack / Beehiiv / Ghost / Medium / Brevo / custom (Phase 12 W3.1) |
22
25
 
23
26
  **Requires `OPEDD_BUYER_TOKEN` (opedd_buyer_live_*)**
24
27
 
@@ -26,26 +29,38 @@ Exposes up to 11 tools to any AI assistant (some are conditional on env vars):
26
29
  |------|-------------|
27
30
  | `get_content` | Retrieve a licensed article — includes 7 Phase 11 M2 RAG metadata fields (author, language, word_count, content_hash, image_urls, canonical_url, tags) |
28
31
 
29
- **Requires `OPEDD_ACCESS_KEY` (eak_*) — buyer-side feed surfaces**
32
+ **Requires `OPEDD_ACCESS_KEY` (ent_*) — buyer-side feed surfaces**
30
33
 
31
34
  | Tool | Description |
32
35
  |------|-------------|
33
36
  | `list_feed` | List articles from a buyer's licensed catalog with `since` delta-feed support (Phase 11 M5) |
34
37
  | `stream_feed_ndjson` | Bulk-export up to 1000 articles per call via NDJSON wire format (Phase 11 M3) |
35
38
 
36
- **Requires `OPEDD_BUYER_JWT` (Supabase JWT) — audit + compliance surfaces**
39
+ **Requires `OPEDD_BUYER_JWT` (Supabase JWT) — buyer account + audit + compliance + EU AI Act surfaces**
37
40
 
38
41
  | Tool | Description |
39
42
  |------|-------------|
43
+ | `get_buyer_account` | Fetch buyer profile + masked API key list — buyer-dashboard mental model |
40
44
  | `get_audit_events` | Per-event audit ledger with Tempo on-chain attestation inclusion proofs inline (Phase 9.x + 10 M5) |
41
45
  | `get_compliance_dossier` | Procurement-defense compliance dossier mapping retrievals to license terms (Phase 11 M4) |
46
+ | `article_53_attestation` | Signed JWT attesting EU AI Act Article 53(1)(d) compliance for a license — the artifact AI labs hand to legal/procurement (Phase 12 W1.4) |
42
47
 
43
- **Requires `OPEDD_API_KEY` (op_*) — publisher-side**
48
+ **Requires `OPEDD_PUB_BEARER` (opedd_pub_*) — publisher-side**
44
49
 
45
50
  | Tool | Description |
46
51
  |------|-------------|
47
52
  | `list_publisher_content` | List your own articles with pricing and stats |
48
53
 
54
+ ### Regulatory framing (CDSM Article 4 vs EU AI Act Article 53 — never conflated)
55
+
56
+ Per the [opedd-backend INVARIANTS.md W1.6 amendment](https://github.com/Opedd/opedd-backend/blob/main/INVARIANTS.md):
57
+
58
+ - **`rsl_get(publisher_id, jsonld=true)`** → publisher-side **CDSM Article 4(3)** opt-out declaration (signed JSON-LD receipt over the reservation state).
59
+ - **`article_53_attestation(license_id)`** → buyer-side **EU AI Act Article 53** attestation (signed HS256 JWT scoped to one license).
60
+ - **`get_compliance_dossier(from, to)`** → comprehensive procurement-defense dossier covering BOTH frameworks (publisher CDSM reservation honored + buyer Article 53 evidence chain).
61
+
62
+ These serve different audit-defensibility modes and never share wire format. Tool descriptions cite the W1.6 invariant inline.
63
+
49
64
  ## Install
50
65
 
51
66
  ```bash
@@ -66,10 +81,11 @@ Set environment variables to pre-configure the server:
66
81
  |----------|----------|-------------|
67
82
  | `OPEDD_BUYER_EMAIL` | Recommended | Your email — used as default for all purchases |
68
83
  | `OPEDD_PAYMENT_METHOD_ID` | Recommended | Stripe `pm_...` ID — used for autonomous per-article purchasing |
69
- | `OPEDD_BUYER_TOKEN` | Optional | Buyer API token (`opedd_buyer_live_*` canonical post-5.2.1a; `bk_live_*` legacy) — enables `get_content` |
70
- | `OPEDD_ACCESS_KEY` | Optional | Enterprise access key (`eak_*`) — enables `list_feed` + `stream_feed_ndjson` |
84
+ | `OPEDD_BUYER_TOKEN` | Optional | Buyer API token (`opedd_buyer_live_*` canonical; `opedd_buyer_test_*` for sandbox) — enables `get_content` |
85
+ | `OPEDD_ACCESS_KEY` | Optional | Enterprise access key (`ent_*`) — enables `list_feed` + `stream_feed_ndjson` |
71
86
  | `OPEDD_BUYER_JWT` | Optional | Supabase session JWT from the buyer portal — enables `get_audit_events` + `get_compliance_dossier` |
72
- | `OPEDD_API_KEY` | Optional | Publisher API key (`op_...`) — enables `list_publisher_content` |
87
+ | `OPEDD_PUB_BEARER` | Optional | Canonical Publisher API Bearer key (`opedd_pub_<env>_<32-hex>`; issued via `POST /publishers-api-keys action=create_api_key`) — enables `list_publisher_content`. **v0.4.0 canonical.** |
88
+ | `OPEDD_API_KEY` | Deprecated | Legacy Publisher API key (`op_...`) — fallback during the transition window; will stop working when opedd-backend Phase C deploys. Migrate to `OPEDD_PUB_BEARER`. |
73
89
  | `OPEDD_API_URL` | Optional | Override the API base URL (default: Opedd production) |
74
90
 
75
91
  > **Getting a Stripe payment method ID**: Save a card in your Stripe account and retrieve the `pm_...` ID via the [Stripe API](https://stripe.com/docs/api/payment_methods).
@@ -121,6 +137,20 @@ Once configured, you can ask your AI assistant:
121
137
 
122
138
  > "Browse the latest licenses from The Information publisher."
123
139
 
140
+ ### Phase 12 Wave 1 + 3 surfaces (v0.3.0)
141
+
142
+ > "What does publisher 8268c353-ffa3-4db3-bbb2-90ddbbb43e41 license? Pull the RSL manifest." — uses `rsl_get`
143
+
144
+ > "Get me a CDSM Article 4(3) signed receipt for that publisher's reservation state — set `jsonld: true`." — uses `rsl_get` with content negotiation
145
+
146
+ > "Generate an EU AI Act Article 53 attestation JWT for our license `11111111-...` covering the last 90 days. I need the artifact for our procurement audit committee." — uses `article_53_attestation` (requires `OPEDD_BUYER_JWT`)
147
+
148
+ > "This publisher just signed up: `https://noahpinion.substack.com`. Which onboarding workflow should we use?" — uses `detect_platform`
149
+
150
+ > "Browse the finance category in the Opedd catalog — show me publishers with at least 50 articles." — uses `publisher_directory`
151
+
152
+ > "What's in my buyer account? Show me my active API keys." — uses `get_buyer_account` (requires `OPEDD_BUYER_JWT`)
153
+
124
154
  The assistant will call the appropriate Opedd tools, show you the results, and—if you've pre-configured a payment method—can complete a purchase autonomously.
125
155
 
126
156
  ## Payment methods
package/dist/index.js CHANGED
@@ -2,14 +2,15 @@
2
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
+ import { pathToFileURL } from "node:url";
5
6
  // ─── Configuration ────────────────────────────────────────────────────────────
6
7
  const API_BASE = process.env.OPEDD_API_URL ??
7
8
  "https://api.opedd.com";
8
9
  const BUYER_EMAIL = process.env.OPEDD_BUYER_EMAIL;
9
10
  const PAYMENT_METHOD_ID = process.env.OPEDD_PAYMENT_METHOD_ID;
10
11
  const API_KEY = process.env.OPEDD_API_KEY; // publisher API key (op_...)
11
- const BUYER_TOKEN = process.env.OPEDD_BUYER_TOKEN; // buyer API token (opedd_buyer_live_... or bk_live_...)
12
- const ACCESS_KEY = process.env.OPEDD_ACCESS_KEY; // enterprise access key (eak_*); for /enterprise-license GET feed
12
+ const BUYER_TOKEN = process.env.OPEDD_BUYER_TOKEN; // buyer API token (opedd_buyer_live_... or opedd_buyer_test_...)
13
+ const ACCESS_KEY = process.env.OPEDD_ACCESS_KEY; // enterprise access key (ent_*); for /enterprise-license GET feed
13
14
  const BUYER_JWT = process.env.OPEDD_BUYER_JWT; // Supabase JWT; for /buyer-audit + /buyer-compliance-report
14
15
  // ─── HTTP helpers ─────────────────────────────────────────────────────────────
15
16
  async function opeddFetch(path, options = {}) {
@@ -73,7 +74,7 @@ function err(msg) {
73
74
  return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
74
75
  }
75
76
  // ─── Tool definitions ─────────────────────────────────────────────────────────
76
- const TOOLS = [
77
+ export const TOOLS = [
77
78
  {
78
79
  name: "lookup_content",
79
80
  description: "Look up a piece of content on the Opedd registry by URL. " +
@@ -180,12 +181,101 @@ const TOOLS = [
180
181
  },
181
182
  },
182
183
  },
184
+ // ─── Public buyer-discovery — catalog browse ──────────────────────────────
185
+ {
186
+ name: "publisher_directory",
187
+ description: "Browse the public Opedd publisher catalog via GET /publisher-directory. " +
188
+ "Returns paginated publishers with article counts, pricing (per-article + annual + monthly-forward-feed), " +
189
+ "plan, and sample articles (RAG-extended metadata). " +
190
+ "**The primary discovery surface for AI labs to find Opedd-licensable publishers** — distinct from " +
191
+ "`browse_registry` (which lists issued LICENSES, not publishers). " +
192
+ "Filter by category (case-insensitive substring), min_articles, or verified status. " +
193
+ "Public no-auth — useful pre-purchase scoping before buyers commit to enterprise-license POST.",
194
+ inputSchema: {
195
+ type: "object",
196
+ properties: {
197
+ category: {
198
+ type: "string",
199
+ description: "Case-insensitive substring filter on publisher category (e.g. 'finance', 'AI').",
200
+ },
201
+ min_articles: {
202
+ type: "number",
203
+ description: "Filter to publishers with at least this many licensable articles.",
204
+ },
205
+ verified: {
206
+ type: "string",
207
+ description: "'true' to show only verified publishers (default), 'false' for unverified.",
208
+ },
209
+ limit: {
210
+ type: "number",
211
+ description: "Page size cap.",
212
+ },
213
+ offset: {
214
+ type: "number",
215
+ description: "Pagination offset.",
216
+ },
217
+ },
218
+ },
219
+ },
220
+ // ─── Phase 12 Wave 3 W3.1 — onboarding helpers ────────────────────────────
221
+ {
222
+ name: "detect_platform",
223
+ description: "Detect the content platform behind a URL via POST /detect-platform (Phase 12 Wave 3 W3.1). " +
224
+ "Public no-auth lookup. Given a URL, identifies what platform powers it " +
225
+ "(Substack / Beehiiv / Ghost / Medium / Brevo / custom) and returns the suggested onboarding workflow. " +
226
+ "Hostname-detectable platforms (Substack subdomain, Beehiiv suffix, etc.) resolve in milliseconds; " +
227
+ "custom domains may take ~few seconds while the detector probes well-known platform endpoints in parallel. " +
228
+ "Returns: {platform, confidence, archive_method, forward_method, required_credentials, instructions}. " +
229
+ "The archive_method + forward_method fields are the two onboarding-workflow inputs Opedd's setup wizard reads " +
230
+ "(one for historical content backfill, one for new-content forward stream). " +
231
+ "instructions is human-readable operator copy explaining the inferred path.",
232
+ inputSchema: {
233
+ type: "object",
234
+ required: ["url"],
235
+ properties: {
236
+ url: {
237
+ type: "string",
238
+ description: "Publisher URL to inspect (any well-formed URL works; hostname-match short-circuits the probe path).",
239
+ },
240
+ },
241
+ },
242
+ },
243
+ // ─── Phase 12 Wave 1 W1.1 — public RSL Standard manifest ─────────────────
244
+ {
245
+ name: "rsl_get",
246
+ description: "Fetch a publisher's RSL Standard manifest via GET /rsl-manifest (Phase 12 Wave 1 W1.1). " +
247
+ "Public no-auth endpoint — discovery surface for AI agents/crawlers wanting to know what's licensable " +
248
+ "from a publisher BEFORE going through the buyer-account signup flow. " +
249
+ "Returns the 4 canonical license types (ai_retrieval, ai_training, human_per_article, human_full_archive) " +
250
+ "the publisher has opted into, plus the EU CDSM Article 4(3) opt-out posture (`tdm_reservation`). " +
251
+ "Set `jsonld: true` to request the JSON-LD shape with embedded HMAC-SHA256 signed receipt over the " +
252
+ "CDSM Article 4(3) reservation state + `tdm:reservationSignedAt` timestamp — regulators can post-hoc " +
253
+ "verify the reservation was the claimed value at the claimed time. Default `jsonld: false` returns " +
254
+ "the raw RSL Standard JSON manifest. " +
255
+ "Per INVARIANTS.md W1.6: this is the PUBLISHER-side CDSM Article 4(3) declaration surface. It is NOT " +
256
+ "an EU AI Act Article 53 attestation (which is buyer-side, JWT-auth, via article_53_attestation tool).",
257
+ inputSchema: {
258
+ type: "object",
259
+ required: ["publisher_id"],
260
+ properties: {
261
+ publisher_id: {
262
+ type: "string",
263
+ description: "UUID of the publisher whose RSL manifest to fetch. Publisher must be verified.",
264
+ },
265
+ jsonld: {
266
+ type: "boolean",
267
+ description: "If true, request JSON-LD shape (Accept: application/ld+json) with embedded HMAC-SHA256 signed receipt. " +
268
+ "Default false returns raw RSL Standard JSON shape.",
269
+ },
270
+ },
271
+ },
272
+ },
183
273
  // ─── Phase 10 + 11 buyer-side surfaces (M6.4) ─────────────────────────────
184
274
  {
185
275
  name: "purchase_enterprise_license",
186
276
  description: "Purchase a bulk enterprise license covering multiple publishers (Phase 10). " +
187
277
  "Returns a Stripe client_secret for payment completion + the enterprise_license_id. " +
188
- "After payment, an eak_* access key is emailed to buyer_email. " +
278
+ "After payment, an ent_* access key is emailed to buyer_email. " +
189
279
  "Scopes: 'custom' (pass-through publisher_ids), 'platform_wide' (auto-resolve all opted-in publishers), 'filtered' (Phase 10 filter_rules). " +
190
280
  "License tiers: 'rag' (= ai_retrieval), 'training' (= ai_training, flat-fee not metered), 'inference' (= ai_retrieval), 'full_ai' (writes both retrieval + training records).",
191
281
  inputSchema: {
@@ -240,7 +330,7 @@ const TOOLS = [
240
330
  if (BUYER_TOKEN) {
241
331
  TOOLS.push({
242
332
  name: "get_content",
243
- description: "Retrieve the full body of a licensed article using a buyer API token (opedd_buyer_live_* canonical post-5.2.1a; bk_live_* legacy). " +
333
+ description: "Retrieve the full body of a licensed article using a buyer API token (opedd_buyer_live_* canonical; opedd_buyer_test_* for sandbox). " +
244
334
  "Requires OPEDD_BUYER_TOKEN env var (create one at opedd.com/licenses after purchasing). " +
245
335
  "Works for per-article licenses (token scoped to that article) and archive licenses (token covers all publisher content). " +
246
336
  "The publisher must have content delivery enabled and must have pushed content for the article. " +
@@ -257,7 +347,7 @@ if (BUYER_TOKEN) {
257
347
  },
258
348
  buyer_token: {
259
349
  type: "string",
260
- description: "Buyer API token (opedd_buyer_live_* or bk_live_*). Falls back to OPEDD_BUYER_TOKEN env var.",
350
+ description: "Buyer API token (opedd_buyer_live_* or opedd_buyer_test_*). Falls back to OPEDD_BUYER_TOKEN env var.",
261
351
  },
262
352
  },
263
353
  },
@@ -271,7 +361,7 @@ if (ACCESS_KEY) {
271
361
  "Returns JSON-format response with paginated articles. " +
272
362
  "Use `since` (ISO 8601) for delta-feed polling — only articles published after the timestamp. " +
273
363
  "Use `cursor` for pagination across pages. " +
274
- "Requires OPEDD_ACCESS_KEY (eak_* enterprise access key). " +
364
+ "Requires OPEDD_ACCESS_KEY (ent_* enterprise access key). " +
275
365
  "For larger bulk corpus pulls, use stream_feed_ndjson (up to 1000 articles per call vs 200 here).",
276
366
  inputSchema: {
277
367
  type: "object",
@@ -299,7 +389,7 @@ if (ACCESS_KEY) {
299
389
  "Use `since` (ISO 8601) for delta-feed. Use `cursor` to paginate beyond 1000. " +
300
390
  "Backend supports 5000 articles per call; the MCP cap is 1000 for transport reasonability. " +
301
391
  "Real bulk-ingest pipelines should use the Python SDK (pip install opedd) directly — not via MCP. " +
302
- "Requires OPEDD_ACCESS_KEY (eak_*).",
392
+ "Requires OPEDD_ACCESS_KEY (ent_*).",
303
393
  inputSchema: {
304
394
  type: "object",
305
395
  properties: {
@@ -356,6 +446,57 @@ if (BUYER_JWT) {
356
446
  },
357
447
  },
358
448
  });
449
+ TOOLS.push({
450
+ name: "get_buyer_account",
451
+ description: "Fetch the authenticated buyer's account profile + masked API key list via GET /buyer-account. " +
452
+ "Returns the enterprise_buyers row (contact_email, buyer_org, created_at, etc.) plus a list of all " +
453
+ "buyer-side API keys with masked prefixes (NEVER plaintext post-issuance — only the 12-char " +
454
+ "key_prefix is returned, e.g. 'opedd_buyer_'). " +
455
+ "Use cases: post-signup verification ('what was just issued to me?'), buyer dashboard mental model " +
456
+ "('what licenses do I currently hold?'), audit prep ('show me the key list before rotation'). " +
457
+ "For full mid-lifecycle license details (filter_rules, billing, payouts), buyers consult the buyer portal at opedd.com/buyer. " +
458
+ "Requires OPEDD_BUYER_JWT.",
459
+ inputSchema: {
460
+ type: "object",
461
+ properties: {},
462
+ },
463
+ });
464
+ TOOLS.push({
465
+ name: "article_53_attestation",
466
+ description: "Issue a signed JWT attesting to EU AI Act Article 53 compliance for a specific license via " +
467
+ "GET /eu-ai-act/article-53-attestation (Phase 12 Wave 1 W1.4). " +
468
+ "Returns a freshly-signed HS256 JWT regulators can verify offline against the canonical signing key. " +
469
+ "Embeds: license context, usage-count over the attestation window, the most-recent Tempo Merkle root, " +
470
+ "and canonical claims (iss/sub/iat/exp/jti/aud). " +
471
+ "**The artifact AI labs hand to legal/procurement for EU AI Act Article 53(1)(d) transparency-obligation evidence.** " +
472
+ "Per INVARIANTS.md W1.6: this attests to EU AI Act Article 53 ONLY (buyer-side GPAI-model-provider " +
473
+ "transparency obligation). It does NOT discharge a publisher's CDSM Article 4(3) reservation obligation — " +
474
+ "that lives on the rsl_get tool (jsonld=true variant). Never conflate. " +
475
+ "Optional `content_id` scopes the attestation to one article; default is license-wide. " +
476
+ "Window cap: 365 days. Requires OPEDD_BUYER_JWT.",
477
+ inputSchema: {
478
+ type: "object",
479
+ required: ["license_id"],
480
+ properties: {
481
+ license_id: {
482
+ type: "string",
483
+ description: "UUID of the enterprise_license OR legacy individual license to attest. Buyer must own it.",
484
+ },
485
+ content_id: {
486
+ type: "string",
487
+ description: "Optional UUID of a specific article to scope the attestation. Default: license-wide.",
488
+ },
489
+ window_start: {
490
+ type: "string",
491
+ description: "ISO 8601 lower bound of the attestation window. Default: now - 90 days.",
492
+ },
493
+ window_end: {
494
+ type: "string",
495
+ description: "ISO 8601 upper bound. Default: now. Window may not exceed 365 days (hard cap).",
496
+ },
497
+ },
498
+ },
499
+ });
359
500
  TOOLS.push({
360
501
  name: "get_compliance_dossier",
361
502
  description: "Generate a procurement-defense compliance dossier via GET /buyer-compliance-report (Phase 11 M4). " +
@@ -413,10 +554,17 @@ if (API_KEY) {
413
554
  });
414
555
  }
415
556
  // ─── MCP Server ───────────────────────────────────────────────────────────────
416
- const server = new Server({ name: "opedd-mcp", version: "0.2.0" }, { capabilities: { tools: {} } });
557
+ const server = new Server({ name: "opedd-mcp", version: "0.3.0" }, { capabilities: { tools: {} } });
417
558
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
418
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
419
- const { name, arguments: args = {} } = request.params;
559
+ server.setRequestHandler(CallToolRequestSchema, async (request) => dispatchTool(request.params.name, (request.params.arguments ?? {})));
560
+ // ─── Tool dispatcher (exported for unit tests) ────────────────────────────────
561
+ //
562
+ // Extracted from the inline handler 2026-05-24 EEST as part of the opedd-mcp
563
+ // test cohort ship. Behavior-preserving: the server.setRequestHandler above
564
+ // is a thin delegation to dispatchTool. Exporting allows unit tests to mock
565
+ // global fetch + invoke each tool's handler directly without spawning a
566
+ // stdio subprocess.
567
+ export async function dispatchTool(name, args) {
420
568
  try {
421
569
  switch (name) {
422
570
  // ── lookup_content ─────────────────────────────────────────────────────
@@ -490,6 +638,44 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
490
638
  const data = await opeddFetch(`/content-delivery?article_id=${encodeURIComponent(article_id)}`, { headers: { Authorization: `Bearer ${token}` } });
491
639
  return ok(data);
492
640
  }
641
+ // ── publisher_directory (buyer-discovery catalog browse) ───────────────
642
+ case "publisher_directory": {
643
+ const { category, min_articles, verified, limit, offset } = args;
644
+ const params = new URLSearchParams();
645
+ if (category)
646
+ params.set("category", category);
647
+ if (min_articles !== undefined)
648
+ params.set("min_articles", String(min_articles));
649
+ if (verified !== undefined)
650
+ params.set("verified", verified);
651
+ if (limit !== undefined)
652
+ params.set("limit", String(limit));
653
+ if (offset !== undefined)
654
+ params.set("offset", String(offset));
655
+ const query = params.toString();
656
+ const data = await opeddFetch(`/publisher-directory${query ? `?${query}` : ""}`);
657
+ return ok(data);
658
+ }
659
+ // ── detect_platform (Phase 12 Wave 3 W3.1) ─────────────────────────────
660
+ case "detect_platform": {
661
+ const { url } = args;
662
+ if (!url)
663
+ return err("url is required");
664
+ const data = await opeddFetch("/detect-platform", {
665
+ method: "POST",
666
+ body: JSON.stringify({ url }),
667
+ });
668
+ return ok(data);
669
+ }
670
+ // ── rsl_get (Phase 12 Wave 1 W1.1) ─────────────────────────────────────
671
+ case "rsl_get": {
672
+ const { publisher_id, jsonld = false } = args;
673
+ if (!publisher_id)
674
+ return err("publisher_id is required");
675
+ const accept = jsonld ? "application/ld+json" : "application/json";
676
+ const data = await opeddFetch(`/rsl-manifest?publisher_id=${encodeURIComponent(publisher_id)}`, { headers: { Accept: accept } });
677
+ return ok(data);
678
+ }
493
679
  // ── purchase_enterprise_license (Phase 10) ─────────────────────────────
494
680
  case "purchase_enterprise_license": {
495
681
  const { publisher_ids, buyer_email: pelEmail, buyer_org, billing_type = "annual", license_tier = "rag", duration_months = 12, scope = "custom", filter_rules, buyer_webhook_url, } = args;
@@ -522,7 +708,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
522
708
  // ── list_feed (Phase 10 + 11) ──────────────────────────────────────────
523
709
  case "list_feed": {
524
710
  if (!ACCESS_KEY) {
525
- return err("OPEDD_ACCESS_KEY env var is required for this tool (eak_* enterprise access key)");
711
+ return err("OPEDD_ACCESS_KEY env var is required for this tool (ent_* enterprise access key)");
526
712
  }
527
713
  const { since, cursor, limit = 50 } = args;
528
714
  const params = new URLSearchParams({
@@ -540,7 +726,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
540
726
  // ── stream_feed_ndjson (Phase 11 M3) ───────────────────────────────────
541
727
  case "stream_feed_ndjson": {
542
728
  if (!ACCESS_KEY) {
543
- return err("OPEDD_ACCESS_KEY env var is required for this tool (eak_* enterprise access key)");
729
+ return err("OPEDD_ACCESS_KEY env var is required for this tool (ent_* enterprise access key)");
544
730
  }
545
731
  const { since, cursor, limit = 200 } = args;
546
732
  const params = new URLSearchParams({
@@ -577,6 +763,34 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
577
763
  });
578
764
  return ok(data);
579
765
  }
766
+ // ── get_buyer_account (buyer profile + masked key list) ────────────────
767
+ case "get_buyer_account": {
768
+ if (!BUYER_JWT) {
769
+ return err("OPEDD_BUYER_JWT env var is required for this tool (Supabase session JWT)");
770
+ }
771
+ const data = await opeddFetch("/buyer-account", {
772
+ headers: { Authorization: `Bearer ${BUYER_JWT}` },
773
+ });
774
+ return ok(data);
775
+ }
776
+ // ── article_53_attestation (Phase 12 Wave 1 W1.4) ──────────────────────
777
+ case "article_53_attestation": {
778
+ if (!BUYER_JWT) {
779
+ return err("OPEDD_BUYER_JWT env var is required for this tool (Supabase session JWT)");
780
+ }
781
+ const { license_id, content_id, window_start, window_end } = args;
782
+ if (!license_id)
783
+ return err("license_id is required");
784
+ const params = new URLSearchParams({ license_id });
785
+ if (content_id)
786
+ params.set("content_id", content_id);
787
+ if (window_start)
788
+ params.set("window_start", window_start);
789
+ if (window_end)
790
+ params.set("window_end", window_end);
791
+ const data = await opeddFetch(`/eu-ai-act/article-53-attestation?${params.toString()}`, { headers: { Authorization: `Bearer ${BUYER_JWT}` } });
792
+ return ok(data);
793
+ }
580
794
  // ── get_compliance_dossier (Phase 11 M4) ───────────────────────────────
581
795
  case "get_compliance_dossier": {
582
796
  if (!BUYER_JWT) {
@@ -621,8 +835,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
621
835
  const msg = e instanceof Error ? e.message : String(e);
622
836
  return err(`Request failed: ${msg}`);
623
837
  }
624
- });
838
+ }
625
839
  // ─── Start ────────────────────────────────────────────────────────────────────
626
- const transport = new StdioServerTransport();
627
- await server.connect(transport);
628
- console.error("[opedd-mcp] Server running on stdio");
840
+ // Skip stdio bootstrap when imported as a module (unit tests need TOOLS +
841
+ // dispatchTool without the server.connect side-effect). Detect via
842
+ // `import.meta.url === pathToFileURL(process.argv[1]).href` the canonical
843
+ // "am I the entry point" check for ESM Node modules.
844
+ if (typeof process !== "undefined" &&
845
+ process.argv?.[1] &&
846
+ import.meta.url === pathToFileURL(process.argv[1]).href) {
847
+ const transport = new StdioServerTransport();
848
+ await server.connect(transport);
849
+ console.error("[opedd-mcp] Server running on stdio");
850
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opedd-mcp",
3
- "version": "0.2.0",
4
- "description": "MCP server for Opedd — discover, purchase, verify, and audit content licenses from AI assistants. Covers Phase 11 buyer-side surfaces.",
3
+ "version": "0.4.0",
4
+ "description": "MCP server for Opedd — discover, purchase, verify, and audit content licenses from AI assistants. Covers Phase 11 buyer-side surfaces + Phase 12 Wave 1 + 3 regulatory surfaces (CDSM Article 4(3) RSL + EU AI Act Article 53 attestation + platform onboarding helper).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
@@ -10,7 +10,9 @@
10
10
  "scripts": {
11
11
  "build": "tsc",
12
12
  "start": "node dist/index.js",
13
- "dev": "tsx src/index.ts"
13
+ "dev": "tsx src/index.ts",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest"
14
16
  },
15
17
  "keywords": [
16
18
  "mcp",
@@ -26,11 +28,12 @@
26
28
  "@modelcontextprotocol/sdk": "^1.0.0"
27
29
  },
28
30
  "devDependencies": {
29
- "@types/node": "^22.0.0",
31
+ "@types/node": "^22.19.19",
30
32
  "tsx": "^4.0.0",
31
- "typescript": "^5.0.0"
33
+ "typescript": "^5.0.0",
34
+ "vitest": "^4.1.7"
32
35
  },
33
36
  "engines": {
34
- "node": ">=18"
37
+ "node": ">=20"
35
38
  }
36
39
  }