opedd-mcp 0.1.1 → 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.
@@ -0,0 +1,53 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ node-version: ["18", "20", "22"]
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Set up Node.js ${{ matrix.node-version }}
20
+ uses: actions/setup-node@v4
21
+ with:
22
+ node-version: ${{ matrix.node-version }}
23
+ cache: npm
24
+
25
+ - name: Install dependencies
26
+ run: npm ci
27
+
28
+ - name: TypeScript compile
29
+ run: npm run build
30
+
31
+ - name: Verify dist/index.js produced
32
+ run: |
33
+ test -f dist/index.js || (echo "::error::dist/index.js not produced by tsc"; exit 1)
34
+ echo "dist/index.js bytes: $(wc -c < dist/index.js)"
35
+
36
+ - name: Smoke-boot the MCP server
37
+ run: |
38
+ # Send initialize + tools/list, confirm v0.2.0 boots and lists tools
39
+ printf '%s\n%s\n' \
40
+ '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"ci","version":"1"}}}' \
41
+ '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
42
+ | timeout 10 node dist/index.js > /tmp/mcp-output.txt 2> /tmp/mcp-stderr.txt || true
43
+ if ! grep -q '"serverInfo":{"name":"opedd-mcp"' /tmp/mcp-output.txt; then
44
+ echo "::error::MCP server did not respond with expected serverInfo"
45
+ cat /tmp/mcp-output.txt
46
+ cat /tmp/mcp-stderr.txt
47
+ exit 1
48
+ fi
49
+ if ! grep -q '"name":"lookup_content"' /tmp/mcp-output.txt; then
50
+ echo "::error::MCP server did not list lookup_content tool"
51
+ exit 1
52
+ fi
53
+ echo "MCP smoke boot OK"
package/README.md CHANGED
@@ -1,18 +1,50 @@
1
1
  # opedd-mcp
2
2
 
3
- An [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server for the [Opedd](https://opedd.com) licensing protocol. Lets AI assistants (Claude Desktop, Cursor, Windsurf, or any MCP-compatible host) discover, purchase, and verify content licenses autonomously — mid-conversation, without opening a browser.
3
+ Licensed content retrieval for AI agents via [MCP (Model Context Protocol)](https://modelcontextprotocol.io). The alternative to unlicensed web scraping for RAG pipelines, AI search, and LLM grounding.
4
+
5
+ Lets AI assistants (Claude Desktop, Cursor, Windsurf, or any MCP-compatible host) discover, purchase, and verify content licenses autonomously — mid-conversation, without opening a browser. Every license is registered on-chain (Tempo blockchain) with cryptographic proof.
6
+
7
+ **Unlike generic search APIs**, Opedd returns content with a verifiable license key — defensible under the EU AI Act and any copyright jurisdiction.
4
8
 
5
9
  ## What it does
6
10
 
7
- Exposes 4–5 tools to any AI assistant:
11
+ Exposes up to 11 tools to any AI assistant (some are conditional on env vars):
12
+
13
+ **Always available — discovery + per-article purchase**
8
14
 
9
15
  | Tool | Description |
10
16
  |------|-------------|
11
17
  | `lookup_content` | Look up an article by URL — returns title, publisher, pricing |
12
- | `purchase_license` | Buy a license with a Stripe payment method — returns license key |
18
+ | `purchase_license` | Buy a single-article license via Stripe — returns OP-XXXX-XXXX key |
13
19
  | `verify_license` | Verify a license key — returns validity, article, publisher, blockchain proof |
14
20
  | `browse_registry` | Browse the public Opedd registry (global or by publisher) |
15
- | `list_publisher_content` | *(requires API key)* List your own articles with pricing and stats |
21
+ | `purchase_enterprise_license` | Buy a bulk enterprise license covering multiple publishers (Phase 10) returns Stripe `client_secret` |
22
+
23
+ **Requires `OPEDD_BUYER_TOKEN` (opedd_buyer_live_*)**
24
+
25
+ | Tool | Description |
26
+ |------|-------------|
27
+ | `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
+
29
+ **Requires `OPEDD_ACCESS_KEY` (eak_*) — buyer-side feed surfaces**
30
+
31
+ | Tool | Description |
32
+ |------|-------------|
33
+ | `list_feed` | List articles from a buyer's licensed catalog with `since` delta-feed support (Phase 11 M5) |
34
+ | `stream_feed_ndjson` | Bulk-export up to 1000 articles per call via NDJSON wire format (Phase 11 M3) |
35
+
36
+ **Requires `OPEDD_BUYER_JWT` (Supabase JWT) — audit + compliance surfaces**
37
+
38
+ | Tool | Description |
39
+ |------|-------------|
40
+ | `get_audit_events` | Per-event audit ledger with Tempo on-chain attestation inclusion proofs inline (Phase 9.x + 10 M5) |
41
+ | `get_compliance_dossier` | Procurement-defense compliance dossier mapping retrievals to license terms (Phase 11 M4) |
42
+
43
+ **Requires `OPEDD_API_KEY` (op_*) — publisher-side**
44
+
45
+ | Tool | Description |
46
+ |------|-------------|
47
+ | `list_publisher_content` | List your own articles with pricing and stats |
16
48
 
17
49
  ## Install
18
50
 
@@ -33,7 +65,10 @@ Set environment variables to pre-configure the server:
33
65
  | Variable | Required | Description |
34
66
  |----------|----------|-------------|
35
67
  | `OPEDD_BUYER_EMAIL` | Recommended | Your email — used as default for all purchases |
36
- | `OPEDD_PAYMENT_METHOD_ID` | Recommended | Stripe `pm_...` ID — used for autonomous purchasing |
68
+ | `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` |
71
+ | `OPEDD_BUYER_JWT` | Optional | Supabase session JWT from the buyer portal — enables `get_audit_events` + `get_compliance_dossier` |
37
72
  | `OPEDD_API_KEY` | Optional | Publisher API key (`op_...`) — enables `list_publisher_content` |
38
73
  | `OPEDD_API_URL` | Optional | Override the API base URL (default: Opedd production) |
39
74
 
@@ -102,6 +137,28 @@ npm run dev # runs with tsx (no build step)
102
137
  npm run build # compiles to dist/
103
138
  ```
104
139
 
140
+ ## How Opedd compares to search APIs
141
+
142
+ | | Opedd | Generic search APIs |
143
+ |---|---|---|
144
+ | **What you get** | Licensed content + license key + on-chain proof — all in one API call | Scraped web content, no rights |
145
+ | **Content delivery** | Full article text delivered via API (JSON), real-time feed via webhooks | Scraped snippets or full-page dumps |
146
+ | **Content quality** | Curated publisher content (niche B2B newsletters, expert analysis) | Whatever's on the open web |
147
+ | **Rights** | Verifiable license key per article, publisher-authorized | No rights clearance |
148
+ | **Proof** | On-chain (Tempo blockchain) — independently verifiable | None |
149
+ | **EU AI Act** | Compliant — full training data provenance chain | No provenance |
150
+ | **Pricing** | Publisher sets their own price per article | Platform decides |
151
+ | **Delivery modes** | Per-article API, bulk feed (JSON firehose), buyer webhooks (push) | Query-response only |
152
+ | **Protocol** | REST + MCP native | REST only |
153
+
154
+ Opedd is the **licensed content delivery layer** for AI — not just a licensing wrapper. Publishers upload content, you retrieve it via API with a license attached. One integration gives your pipeline both the content and the legal right to use it.
155
+
156
+ ## Learn more
157
+
158
+ - [Opedd for AI Agents](https://opedd.com/for-ai-agents) — full documentation, code examples, endpoint reference
159
+ - [API Docs](https://docs.opedd.com) — OpenAPI spec with agent endpoints
160
+ - [Opedd Registry](https://opedd.com/registry) — browse on-chain license proofs
161
+
105
162
  ## License
106
163
 
107
164
  MIT
package/dist/index.js CHANGED
@@ -4,11 +4,13 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
5
  // ─── Configuration ────────────────────────────────────────────────────────────
6
6
  const API_BASE = process.env.OPEDD_API_URL ??
7
- "https://djdzcciayennqchjgybx.supabase.co/functions/v1";
7
+ "https://api.opedd.com";
8
8
  const BUYER_EMAIL = process.env.OPEDD_BUYER_EMAIL;
9
9
  const PAYMENT_METHOD_ID = process.env.OPEDD_PAYMENT_METHOD_ID;
10
10
  const API_KEY = process.env.OPEDD_API_KEY; // publisher API key (op_...)
11
- const BUYER_TOKEN = process.env.OPEDD_BUYER_TOKEN; // buyer API token (bk_live_...)
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
13
+ const BUYER_JWT = process.env.OPEDD_BUYER_JWT; // Supabase JWT; for /buyer-audit + /buyer-compliance-report
12
14
  // ─── HTTP helpers ─────────────────────────────────────────────────────────────
13
15
  async function opeddFetch(path, options = {}) {
14
16
  const url = `${API_BASE}${path}`;
@@ -25,6 +27,45 @@ async function opeddFetch(path, options = {}) {
25
27
  }
26
28
  return body;
27
29
  }
30
+ // Fetch an NDJSON endpoint and collect parsed lines into an array.
31
+ // Last line is typically `{"_meta": {...}}`; surfaced as `meta` field on the
32
+ // returned object so the MCP tool result has both shape pieces inline.
33
+ async function opeddFetchNdjson(path, options = {}) {
34
+ const url = `${API_BASE}${path}`;
35
+ const headers = {
36
+ Accept: "application/x-ndjson",
37
+ ...(API_KEY ? { "X-API-Key": API_KEY } : {}),
38
+ ...(options.headers ?? {}),
39
+ };
40
+ const res = await fetch(url, { ...options, headers });
41
+ if (!res.ok) {
42
+ let msg = `HTTP ${res.status}`;
43
+ try {
44
+ const errBody = await res.json();
45
+ msg = errBody?.error || errBody?.message || msg;
46
+ }
47
+ catch {
48
+ // non-JSON error body — keep the HTTP-status default
49
+ }
50
+ throw new Error(msg);
51
+ }
52
+ const text = await res.text();
53
+ const articles = [];
54
+ let meta = null;
55
+ for (const raw of text.split("\n")) {
56
+ const line = raw.trim();
57
+ if (!line)
58
+ continue;
59
+ const row = JSON.parse(line);
60
+ if (row._meta !== undefined) {
61
+ meta = row._meta;
62
+ }
63
+ else {
64
+ articles.push(row);
65
+ }
66
+ }
67
+ return { articles, meta };
68
+ }
28
69
  function ok(data) {
29
70
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
30
71
  }
@@ -139,15 +180,73 @@ const TOOLS = [
139
180
  },
140
181
  },
141
182
  },
183
+ // ─── Phase 10 + 11 buyer-side surfaces (M6.4) ─────────────────────────────
184
+ {
185
+ name: "purchase_enterprise_license",
186
+ description: "Purchase a bulk enterprise license covering multiple publishers (Phase 10). " +
187
+ "Returns a Stripe client_secret for payment completion + the enterprise_license_id. " +
188
+ "After payment, an eak_* access key is emailed to buyer_email. " +
189
+ "Scopes: 'custom' (pass-through publisher_ids), 'platform_wide' (auto-resolve all opted-in publishers), 'filtered' (Phase 10 filter_rules). " +
190
+ "License tiers: 'rag' (= ai_retrieval), 'training' (= ai_training, flat-fee not metered), 'inference' (= ai_retrieval), 'full_ai' (writes both retrieval + training records).",
191
+ inputSchema: {
192
+ type: "object",
193
+ required: ["publisher_ids", "buyer_email", "buyer_org"],
194
+ properties: {
195
+ publisher_ids: {
196
+ type: "array",
197
+ items: { type: "string" },
198
+ description: "Array of publisher UUIDs. Required for scope='custom'; ignored for platform_wide/filtered (resolved server-side).",
199
+ },
200
+ buyer_email: {
201
+ type: "string",
202
+ description: "Email to deliver the access key after payment",
203
+ },
204
+ buyer_org: {
205
+ type: "string",
206
+ description: "Buyer organization name (for billing + audit ledger)",
207
+ },
208
+ billing_type: {
209
+ type: "string",
210
+ enum: ["annual", "monthly", "annual_plus_monthly"],
211
+ description: "Billing cadence (default: annual)",
212
+ },
213
+ license_tier: {
214
+ type: "string",
215
+ enum: ["rag", "training", "inference", "full_ai"],
216
+ description: "License tier (default: rag)",
217
+ },
218
+ duration_months: {
219
+ type: "number",
220
+ description: "License duration in months (default: 12)",
221
+ },
222
+ scope: {
223
+ type: "string",
224
+ enum: ["custom", "platform_wide", "filtered"],
225
+ description: "Coverage scope (default: custom)",
226
+ },
227
+ filter_rules: {
228
+ type: "object",
229
+ description: "Required when scope='filtered'. See Phase 10 docs for shape: excluded_publisher_ids / direct_license_carveouts / categories / max_price_per_event.",
230
+ },
231
+ buyer_webhook_url: {
232
+ type: "string",
233
+ description: "Optional HMAC-signed webhook for content.published events on covered publishers",
234
+ },
235
+ },
236
+ },
237
+ },
142
238
  ];
143
239
  // If a buyer token is configured, expose content delivery tooling
144
240
  if (BUYER_TOKEN) {
145
241
  TOOLS.push({
146
242
  name: "get_content",
147
- description: "Retrieve the full body of a licensed article using a buyer API token (bk_live_...). " +
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). " +
148
244
  "Requires OPEDD_BUYER_TOKEN env var (create one at opedd.com/licenses after purchasing). " +
149
245
  "Works for per-article licenses (token scoped to that article) and archive licenses (token covers all publisher content). " +
150
- "The publisher must have content delivery enabled and must have pushed content for the article.",
246
+ "The publisher must have content delivery enabled and must have pushed content for the article. " +
247
+ "Phase 11 M2 RAG-extended shape: response includes 7 RAG-essential metadata fields — author, language, word_count, content_hash, image_urls, canonical_url, tags. " +
248
+ "On pre-2026-05-14 historical articles, optional fields (author/language/image_urls/canonical_url/tags) may be NULL. " +
249
+ "NULL means 'data unavailable for this article', NOT 'explicitly empty' — treat as data-missing when filtering; do not interpret as anti-match.",
151
250
  inputSchema: {
152
251
  type: "object",
153
252
  required: ["article_id"],
@@ -158,7 +257,129 @@ if (BUYER_TOKEN) {
158
257
  },
159
258
  buyer_token: {
160
259
  type: "string",
161
- description: "Buyer API token (bk_live_...). Falls back to OPEDD_BUYER_TOKEN env var.",
260
+ description: "Buyer API token (opedd_buyer_live_* or bk_live_*). Falls back to OPEDD_BUYER_TOKEN env var.",
261
+ },
262
+ },
263
+ },
264
+ });
265
+ }
266
+ // If an enterprise access key is configured, expose feed tools
267
+ if (ACCESS_KEY) {
268
+ TOOLS.push({
269
+ name: "list_feed",
270
+ description: "List articles from a buyer's licensed catalog via GET /enterprise-license (Phase 10 + 11). " +
271
+ "Returns JSON-format response with paginated articles. " +
272
+ "Use `since` (ISO 8601) for delta-feed polling — only articles published after the timestamp. " +
273
+ "Use `cursor` for pagination across pages. " +
274
+ "Requires OPEDD_ACCESS_KEY (eak_* enterprise access key). " +
275
+ "For larger bulk corpus pulls, use stream_feed_ndjson (up to 1000 articles per call vs 200 here).",
276
+ inputSchema: {
277
+ type: "object",
278
+ properties: {
279
+ since: {
280
+ type: "string",
281
+ description: "ISO 8601 timestamp — return only articles with published_at > since",
282
+ },
283
+ cursor: {
284
+ type: "string",
285
+ description: "Opaque cursor from prior response's _meta.next_cursor",
286
+ },
287
+ limit: {
288
+ type: "number",
289
+ description: "Max articles per response (default: 50, max: 200)",
290
+ },
291
+ },
292
+ },
293
+ });
294
+ TOOLS.push({
295
+ name: "stream_feed_ndjson",
296
+ description: "Bulk-export a buyer's licensed catalog via GET /enterprise-license?format=ndjson (Phase 11 M3). " +
297
+ "Returns up to 1000 articles per call (collected from line-delimited JSON wire format). " +
298
+ "Each article emits one usage_records row (analytics-only sentinel 'bulk-export:<request_id>:<article_id>' — not metered-billable per the revenue-model bifurcation invariant). " +
299
+ "Use `since` (ISO 8601) for delta-feed. Use `cursor` to paginate beyond 1000. " +
300
+ "Backend supports 5000 articles per call; the MCP cap is 1000 for transport reasonability. " +
301
+ "Real bulk-ingest pipelines should use the Python SDK (pip install opedd) directly — not via MCP. " +
302
+ "Requires OPEDD_ACCESS_KEY (eak_*).",
303
+ inputSchema: {
304
+ type: "object",
305
+ properties: {
306
+ since: {
307
+ type: "string",
308
+ description: "ISO 8601 timestamp — return only articles with published_at > since",
309
+ },
310
+ cursor: {
311
+ type: "string",
312
+ description: "Opaque cursor from prior response's _meta.next_cursor",
313
+ },
314
+ limit: {
315
+ type: "number",
316
+ description: "Max articles per response (default: 200, max: 1000)",
317
+ },
318
+ },
319
+ },
320
+ });
321
+ }
322
+ // If a Supabase buyer JWT is configured, expose audit + compliance tools
323
+ if (BUYER_JWT) {
324
+ TOOLS.push({
325
+ name: "get_audit_events",
326
+ description: "Browse per-event audit rows for the authenticated buyer via GET /buyer-audit (Phase 9.x). " +
327
+ "Each row carries license_terms + Tempo on-chain attestation (merkle_root + inclusion_proof when blockchain_status='confirmed'). " +
328
+ "Optional filter by event_type ('content_access', 'bulk_content_access', 'compliance_report_generated'). " +
329
+ "Window cap 30 days (vs 90-day cap on get_compliance_dossier). " +
330
+ "Attestation inclusion proof is included on every row by default — no separate flag needed (M6.4 consolidation per founder ratification: tools 4 + 6 merged into one cleaner mental model). " +
331
+ "Requires OPEDD_BUYER_JWT (Supabase session JWT from the buyer portal).",
332
+ inputSchema: {
333
+ type: "object",
334
+ properties: {
335
+ from: {
336
+ type: "string",
337
+ description: "ISO 8601 timestamp lower bound (inclusive)",
338
+ },
339
+ to: {
340
+ type: "string",
341
+ description: "ISO 8601 timestamp upper bound (inclusive)",
342
+ },
343
+ event_type: {
344
+ type: "string",
345
+ enum: ["content_access", "bulk_content_access", "compliance_report_generated"],
346
+ description: "Optional event-class filter",
347
+ },
348
+ cursor: {
349
+ type: "string",
350
+ description: "Opaque cursor for pagination",
351
+ },
352
+ limit: {
353
+ type: "number",
354
+ description: "Max events per response (default: 50, max: 200)",
355
+ },
356
+ },
357
+ },
358
+ });
359
+ TOOLS.push({
360
+ name: "get_compliance_dossier",
361
+ description: "Generate a procurement-defense compliance dossier via GET /buyer-compliance-report (Phase 11 M4). " +
362
+ "Per-row dossier shape: 25+ fields including 17 RAG-essential article fields + full license_terms + on_chain_attestation block. " +
363
+ "Bulk envelopes fan out into per-article rows by iterating metadata.article_ids[]. " +
364
+ "Self-audit invariant: every successful call writes one license_events row with event_type='compliance_report_generated' BEFORE returning. " +
365
+ "Window cap: 90 days per call (vs 30-day cap on get_audit_events). For annual audits, paginate via _meta.next_cursor across 4 quarterly windows. " +
366
+ "Compliance framework anchors (boolean flags) map to EU AI Act Article 53, CDSM Article 4(3), on-chain attestation, TDM reservation. " +
367
+ "Requires OPEDD_BUYER_JWT.",
368
+ inputSchema: {
369
+ type: "object",
370
+ required: ["from", "to"],
371
+ properties: {
372
+ from: {
373
+ type: "string",
374
+ description: "ISO 8601 timestamp lower bound (inclusive)",
375
+ },
376
+ to: {
377
+ type: "string",
378
+ description: "ISO 8601 timestamp upper bound (inclusive). Window cap 90 days.",
379
+ },
380
+ cursor: {
381
+ type: "string",
382
+ description: "Opaque cursor for pagination",
162
383
  },
163
384
  },
164
385
  },
@@ -192,7 +413,7 @@ if (API_KEY) {
192
413
  });
193
414
  }
194
415
  // ─── MCP Server ───────────────────────────────────────────────────────────────
195
- const server = new Server({ name: "opedd-mcp", version: "0.1.0" }, { capabilities: { tools: {} } });
416
+ const server = new Server({ name: "opedd-mcp", version: "0.2.0" }, { capabilities: { tools: {} } });
196
417
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
197
418
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
198
419
  const { name, arguments: args = {} } = request.params;
@@ -269,6 +490,115 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
269
490
  const data = await opeddFetch(`/content-delivery?article_id=${encodeURIComponent(article_id)}`, { headers: { Authorization: `Bearer ${token}` } });
270
491
  return ok(data);
271
492
  }
493
+ // ── purchase_enterprise_license (Phase 10) ─────────────────────────────
494
+ case "purchase_enterprise_license": {
495
+ 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;
496
+ if (!Array.isArray(publisher_ids) || publisher_ids.length === 0) {
497
+ if (scope === "custom") {
498
+ return err("publisher_ids array is required for scope='custom'");
499
+ }
500
+ }
501
+ if (!pelEmail)
502
+ return err("buyer_email is required");
503
+ if (!buyer_org)
504
+ return err("buyer_org is required");
505
+ const body = {
506
+ publisher_ids: publisher_ids ?? [],
507
+ buyer_email: pelEmail,
508
+ buyer_org,
509
+ billing_type,
510
+ license_tier,
511
+ duration_months,
512
+ scope,
513
+ ...(filter_rules ? { filter_rules } : {}),
514
+ ...(buyer_webhook_url ? { buyer_webhook_url } : {}),
515
+ };
516
+ const data = await opeddFetch("/enterprise-license", {
517
+ method: "POST",
518
+ body: JSON.stringify(body),
519
+ });
520
+ return ok(data);
521
+ }
522
+ // ── list_feed (Phase 10 + 11) ──────────────────────────────────────────
523
+ case "list_feed": {
524
+ if (!ACCESS_KEY) {
525
+ return err("OPEDD_ACCESS_KEY env var is required for this tool (eak_* enterprise access key)");
526
+ }
527
+ const { since, cursor, limit = 50 } = args;
528
+ const params = new URLSearchParams({
529
+ access_key: ACCESS_KEY,
530
+ format: "json",
531
+ limit: String(Math.min(Number(limit) || 50, 200)),
532
+ });
533
+ if (since)
534
+ params.set("since", since);
535
+ if (cursor)
536
+ params.set("cursor", cursor);
537
+ const data = await opeddFetch(`/enterprise-license?${params.toString()}`);
538
+ return ok(data);
539
+ }
540
+ // ── stream_feed_ndjson (Phase 11 M3) ───────────────────────────────────
541
+ case "stream_feed_ndjson": {
542
+ if (!ACCESS_KEY) {
543
+ return err("OPEDD_ACCESS_KEY env var is required for this tool (eak_* enterprise access key)");
544
+ }
545
+ const { since, cursor, limit = 200 } = args;
546
+ const params = new URLSearchParams({
547
+ access_key: ACCESS_KEY,
548
+ format: "ndjson",
549
+ limit: String(Math.min(Number(limit) || 200, 1000)),
550
+ });
551
+ if (since)
552
+ params.set("since", since);
553
+ if (cursor)
554
+ params.set("cursor", cursor);
555
+ const data = await opeddFetchNdjson(`/enterprise-license?${params.toString()}`);
556
+ return ok(data);
557
+ }
558
+ // ── get_audit_events (Phase 9.x + 10 M5 attestation) ───────────────────
559
+ case "get_audit_events": {
560
+ if (!BUYER_JWT) {
561
+ return err("OPEDD_BUYER_JWT env var is required for this tool (Supabase session JWT)");
562
+ }
563
+ const { from, to, event_type, cursor, limit = 50 } = args;
564
+ const params = new URLSearchParams({
565
+ limit: String(Math.min(Number(limit) || 50, 200)),
566
+ });
567
+ if (from)
568
+ params.set("from", from);
569
+ if (to)
570
+ params.set("to", to);
571
+ if (event_type)
572
+ params.set("event_type", event_type);
573
+ if (cursor)
574
+ params.set("cursor", cursor);
575
+ const data = await opeddFetch(`/buyer-audit?${params.toString()}`, {
576
+ headers: { Authorization: `Bearer ${BUYER_JWT}` },
577
+ });
578
+ return ok(data);
579
+ }
580
+ // ── get_compliance_dossier (Phase 11 M4) ───────────────────────────────
581
+ case "get_compliance_dossier": {
582
+ if (!BUYER_JWT) {
583
+ return err("OPEDD_BUYER_JWT env var is required for this tool (Supabase session JWT)");
584
+ }
585
+ const { from, to, cursor } = args;
586
+ if (!from)
587
+ return err("from (ISO 8601 timestamp) is required");
588
+ if (!to)
589
+ return err("to (ISO 8601 timestamp) is required");
590
+ const params = new URLSearchParams({
591
+ from,
592
+ to,
593
+ format: "json",
594
+ });
595
+ if (cursor)
596
+ params.set("cursor", cursor);
597
+ const data = await opeddFetch(`/buyer-compliance-report?${params.toString()}`, {
598
+ headers: { Authorization: `Bearer ${BUYER_JWT}` },
599
+ });
600
+ return ok(data);
601
+ }
272
602
  // ── list_publisher_content ─────────────────────────────────────────────
273
603
  case "list_publisher_content": {
274
604
  if (!API_KEY) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opedd-mcp",
3
- "version": "0.1.1",
4
- "description": "MCP server for Opedd — discover, purchase, and verify content licenses from AI assistants",
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.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
package/src/index.ts CHANGED
@@ -12,12 +12,14 @@ import {
12
12
 
13
13
  const API_BASE =
14
14
  process.env.OPEDD_API_URL ??
15
- "https://djdzcciayennqchjgybx.supabase.co/functions/v1";
15
+ "https://api.opedd.com";
16
16
 
17
17
  const BUYER_EMAIL = process.env.OPEDD_BUYER_EMAIL;
18
18
  const PAYMENT_METHOD_ID = process.env.OPEDD_PAYMENT_METHOD_ID;
19
19
  const API_KEY = process.env.OPEDD_API_KEY; // publisher API key (op_...)
20
- const BUYER_TOKEN = process.env.OPEDD_BUYER_TOKEN; // buyer API token (bk_live_...)
20
+ const BUYER_TOKEN = process.env.OPEDD_BUYER_TOKEN; // buyer API token (opedd_buyer_live_... or bk_live_...)
21
+ const ACCESS_KEY = process.env.OPEDD_ACCESS_KEY; // enterprise access key (eak_*); for /enterprise-license GET feed
22
+ const BUYER_JWT = process.env.OPEDD_BUYER_JWT; // Supabase JWT; for /buyer-audit + /buyer-compliance-report
21
23
 
22
24
  // ─── HTTP helpers ─────────────────────────────────────────────────────────────
23
25
 
@@ -37,6 +39,46 @@ async function opeddFetch(path: string, options: RequestInit = {}): Promise<unkn
37
39
  return body;
38
40
  }
39
41
 
42
+ // Fetch an NDJSON endpoint and collect parsed lines into an array.
43
+ // Last line is typically `{"_meta": {...}}`; surfaced as `meta` field on the
44
+ // returned object so the MCP tool result has both shape pieces inline.
45
+ async function opeddFetchNdjson(
46
+ path: string,
47
+ options: RequestInit = {},
48
+ ): Promise<{ articles: unknown[]; meta: unknown }> {
49
+ const url = `${API_BASE}${path}`;
50
+ const headers: Record<string, string> = {
51
+ Accept: "application/x-ndjson",
52
+ ...(API_KEY ? { "X-API-Key": API_KEY } : {}),
53
+ ...(options.headers as Record<string, string> ?? {}),
54
+ };
55
+ const res = await fetch(url, { ...options, headers });
56
+ if (!res.ok) {
57
+ let msg = `HTTP ${res.status}`;
58
+ try {
59
+ const errBody = await res.json();
60
+ msg = (errBody as any)?.error || (errBody as any)?.message || msg;
61
+ } catch {
62
+ // non-JSON error body — keep the HTTP-status default
63
+ }
64
+ throw new Error(msg);
65
+ }
66
+ const text = await res.text();
67
+ const articles: unknown[] = [];
68
+ let meta: unknown = null;
69
+ for (const raw of text.split("\n")) {
70
+ const line = raw.trim();
71
+ if (!line) continue;
72
+ const row = JSON.parse(line) as { _meta?: unknown };
73
+ if (row._meta !== undefined) {
74
+ meta = row._meta;
75
+ } else {
76
+ articles.push(row);
77
+ }
78
+ }
79
+ return { articles, meta };
80
+ }
81
+
40
82
  // ─── MCP response helpers ─────────────────────────────────────────────────────
41
83
 
42
84
  type TextContent = { type: "text"; text: string };
@@ -165,6 +207,62 @@ const TOOLS: Tool[] = [
165
207
  },
166
208
  },
167
209
  },
210
+ // ─── Phase 10 + 11 buyer-side surfaces (M6.4) ─────────────────────────────
211
+ {
212
+ name: "purchase_enterprise_license",
213
+ description:
214
+ "Purchase a bulk enterprise license covering multiple publishers (Phase 10). " +
215
+ "Returns a Stripe client_secret for payment completion + the enterprise_license_id. " +
216
+ "After payment, an eak_* access key is emailed to buyer_email. " +
217
+ "Scopes: 'custom' (pass-through publisher_ids), 'platform_wide' (auto-resolve all opted-in publishers), 'filtered' (Phase 10 filter_rules). " +
218
+ "License tiers: 'rag' (= ai_retrieval), 'training' (= ai_training, flat-fee not metered), 'inference' (= ai_retrieval), 'full_ai' (writes both retrieval + training records).",
219
+ inputSchema: {
220
+ type: "object",
221
+ required: ["publisher_ids", "buyer_email", "buyer_org"],
222
+ properties: {
223
+ publisher_ids: {
224
+ type: "array",
225
+ items: { type: "string" },
226
+ description: "Array of publisher UUIDs. Required for scope='custom'; ignored for platform_wide/filtered (resolved server-side).",
227
+ },
228
+ buyer_email: {
229
+ type: "string",
230
+ description: "Email to deliver the access key after payment",
231
+ },
232
+ buyer_org: {
233
+ type: "string",
234
+ description: "Buyer organization name (for billing + audit ledger)",
235
+ },
236
+ billing_type: {
237
+ type: "string",
238
+ enum: ["annual", "monthly", "annual_plus_monthly"],
239
+ description: "Billing cadence (default: annual)",
240
+ },
241
+ license_tier: {
242
+ type: "string",
243
+ enum: ["rag", "training", "inference", "full_ai"],
244
+ description: "License tier (default: rag)",
245
+ },
246
+ duration_months: {
247
+ type: "number",
248
+ description: "License duration in months (default: 12)",
249
+ },
250
+ scope: {
251
+ type: "string",
252
+ enum: ["custom", "platform_wide", "filtered"],
253
+ description: "Coverage scope (default: custom)",
254
+ },
255
+ filter_rules: {
256
+ type: "object",
257
+ description: "Required when scope='filtered'. See Phase 10 docs for shape: excluded_publisher_ids / direct_license_carveouts / categories / max_price_per_event.",
258
+ },
259
+ buyer_webhook_url: {
260
+ type: "string",
261
+ description: "Optional HMAC-signed webhook for content.published events on covered publishers",
262
+ },
263
+ },
264
+ },
265
+ },
168
266
  ];
169
267
 
170
268
  // If a buyer token is configured, expose content delivery tooling
@@ -172,10 +270,13 @@ if (BUYER_TOKEN) {
172
270
  TOOLS.push({
173
271
  name: "get_content",
174
272
  description:
175
- "Retrieve the full body of a licensed article using a buyer API token (bk_live_...). " +
273
+ "Retrieve the full body of a licensed article using a buyer API token (opedd_buyer_live_* canonical post-5.2.1a; bk_live_* legacy). " +
176
274
  "Requires OPEDD_BUYER_TOKEN env var (create one at opedd.com/licenses after purchasing). " +
177
275
  "Works for per-article licenses (token scoped to that article) and archive licenses (token covers all publisher content). " +
178
- "The publisher must have content delivery enabled and must have pushed content for the article.",
276
+ "The publisher must have content delivery enabled and must have pushed content for the article. " +
277
+ "Phase 11 M2 RAG-extended shape: response includes 7 RAG-essential metadata fields — author, language, word_count, content_hash, image_urls, canonical_url, tags. " +
278
+ "On pre-2026-05-14 historical articles, optional fields (author/language/image_urls/canonical_url/tags) may be NULL. " +
279
+ "NULL means 'data unavailable for this article', NOT 'explicitly empty' — treat as data-missing when filtering; do not interpret as anti-match.",
179
280
  inputSchema: {
180
281
  type: "object",
181
282
  required: ["article_id"],
@@ -186,7 +287,135 @@ if (BUYER_TOKEN) {
186
287
  },
187
288
  buyer_token: {
188
289
  type: "string",
189
- description: "Buyer API token (bk_live_...). Falls back to OPEDD_BUYER_TOKEN env var.",
290
+ description: "Buyer API token (opedd_buyer_live_* or bk_live_*). Falls back to OPEDD_BUYER_TOKEN env var.",
291
+ },
292
+ },
293
+ },
294
+ });
295
+ }
296
+
297
+ // If an enterprise access key is configured, expose feed tools
298
+ if (ACCESS_KEY) {
299
+ TOOLS.push({
300
+ name: "list_feed",
301
+ description:
302
+ "List articles from a buyer's licensed catalog via GET /enterprise-license (Phase 10 + 11). " +
303
+ "Returns JSON-format response with paginated articles. " +
304
+ "Use `since` (ISO 8601) for delta-feed polling — only articles published after the timestamp. " +
305
+ "Use `cursor` for pagination across pages. " +
306
+ "Requires OPEDD_ACCESS_KEY (eak_* enterprise access key). " +
307
+ "For larger bulk corpus pulls, use stream_feed_ndjson (up to 1000 articles per call vs 200 here).",
308
+ inputSchema: {
309
+ type: "object",
310
+ properties: {
311
+ since: {
312
+ type: "string",
313
+ description: "ISO 8601 timestamp — return only articles with published_at > since",
314
+ },
315
+ cursor: {
316
+ type: "string",
317
+ description: "Opaque cursor from prior response's _meta.next_cursor",
318
+ },
319
+ limit: {
320
+ type: "number",
321
+ description: "Max articles per response (default: 50, max: 200)",
322
+ },
323
+ },
324
+ },
325
+ });
326
+ TOOLS.push({
327
+ name: "stream_feed_ndjson",
328
+ description:
329
+ "Bulk-export a buyer's licensed catalog via GET /enterprise-license?format=ndjson (Phase 11 M3). " +
330
+ "Returns up to 1000 articles per call (collected from line-delimited JSON wire format). " +
331
+ "Each article emits one usage_records row (analytics-only sentinel 'bulk-export:<request_id>:<article_id>' — not metered-billable per the revenue-model bifurcation invariant). " +
332
+ "Use `since` (ISO 8601) for delta-feed. Use `cursor` to paginate beyond 1000. " +
333
+ "Backend supports 5000 articles per call; the MCP cap is 1000 for transport reasonability. " +
334
+ "Real bulk-ingest pipelines should use the Python SDK (pip install opedd) directly — not via MCP. " +
335
+ "Requires OPEDD_ACCESS_KEY (eak_*).",
336
+ inputSchema: {
337
+ type: "object",
338
+ properties: {
339
+ since: {
340
+ type: "string",
341
+ description: "ISO 8601 timestamp — return only articles with published_at > since",
342
+ },
343
+ cursor: {
344
+ type: "string",
345
+ description: "Opaque cursor from prior response's _meta.next_cursor",
346
+ },
347
+ limit: {
348
+ type: "number",
349
+ description: "Max articles per response (default: 200, max: 1000)",
350
+ },
351
+ },
352
+ },
353
+ });
354
+ }
355
+
356
+ // If a Supabase buyer JWT is configured, expose audit + compliance tools
357
+ if (BUYER_JWT) {
358
+ TOOLS.push({
359
+ name: "get_audit_events",
360
+ description:
361
+ "Browse per-event audit rows for the authenticated buyer via GET /buyer-audit (Phase 9.x). " +
362
+ "Each row carries license_terms + Tempo on-chain attestation (merkle_root + inclusion_proof when blockchain_status='confirmed'). " +
363
+ "Optional filter by event_type ('content_access', 'bulk_content_access', 'compliance_report_generated'). " +
364
+ "Window cap 30 days (vs 90-day cap on get_compliance_dossier). " +
365
+ "Attestation inclusion proof is included on every row by default — no separate flag needed (M6.4 consolidation per founder ratification: tools 4 + 6 merged into one cleaner mental model). " +
366
+ "Requires OPEDD_BUYER_JWT (Supabase session JWT from the buyer portal).",
367
+ inputSchema: {
368
+ type: "object",
369
+ properties: {
370
+ from: {
371
+ type: "string",
372
+ description: "ISO 8601 timestamp lower bound (inclusive)",
373
+ },
374
+ to: {
375
+ type: "string",
376
+ description: "ISO 8601 timestamp upper bound (inclusive)",
377
+ },
378
+ event_type: {
379
+ type: "string",
380
+ enum: ["content_access", "bulk_content_access", "compliance_report_generated"],
381
+ description: "Optional event-class filter",
382
+ },
383
+ cursor: {
384
+ type: "string",
385
+ description: "Opaque cursor for pagination",
386
+ },
387
+ limit: {
388
+ type: "number",
389
+ description: "Max events per response (default: 50, max: 200)",
390
+ },
391
+ },
392
+ },
393
+ });
394
+ TOOLS.push({
395
+ name: "get_compliance_dossier",
396
+ description:
397
+ "Generate a procurement-defense compliance dossier via GET /buyer-compliance-report (Phase 11 M4). " +
398
+ "Per-row dossier shape: 25+ fields including 17 RAG-essential article fields + full license_terms + on_chain_attestation block. " +
399
+ "Bulk envelopes fan out into per-article rows by iterating metadata.article_ids[]. " +
400
+ "Self-audit invariant: every successful call writes one license_events row with event_type='compliance_report_generated' BEFORE returning. " +
401
+ "Window cap: 90 days per call (vs 30-day cap on get_audit_events). For annual audits, paginate via _meta.next_cursor across 4 quarterly windows. " +
402
+ "Compliance framework anchors (boolean flags) map to EU AI Act Article 53, CDSM Article 4(3), on-chain attestation, TDM reservation. " +
403
+ "Requires OPEDD_BUYER_JWT.",
404
+ inputSchema: {
405
+ type: "object",
406
+ required: ["from", "to"],
407
+ properties: {
408
+ from: {
409
+ type: "string",
410
+ description: "ISO 8601 timestamp lower bound (inclusive)",
411
+ },
412
+ to: {
413
+ type: "string",
414
+ description: "ISO 8601 timestamp upper bound (inclusive). Window cap 90 days.",
415
+ },
416
+ cursor: {
417
+ type: "string",
418
+ description: "Opaque cursor for pagination",
190
419
  },
191
420
  },
192
421
  },
@@ -225,7 +454,7 @@ if (API_KEY) {
225
454
  // ─── MCP Server ───────────────────────────────────────────────────────────────
226
455
 
227
456
  const server = new Server(
228
- { name: "opedd-mcp", version: "0.1.0" },
457
+ { name: "opedd-mcp", version: "0.2.0" },
229
458
  { capabilities: { tools: {} } }
230
459
  );
231
460
 
@@ -354,6 +583,153 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
354
583
  return ok(data);
355
584
  }
356
585
 
586
+ // ── purchase_enterprise_license (Phase 10) ─────────────────────────────
587
+ case "purchase_enterprise_license": {
588
+ const {
589
+ publisher_ids,
590
+ buyer_email: pelEmail,
591
+ buyer_org,
592
+ billing_type = "annual",
593
+ license_tier = "rag",
594
+ duration_months = 12,
595
+ scope = "custom",
596
+ filter_rules,
597
+ buyer_webhook_url,
598
+ } = args as {
599
+ publisher_ids?: string[];
600
+ buyer_email?: string;
601
+ buyer_org?: string;
602
+ billing_type?: string;
603
+ license_tier?: string;
604
+ duration_months?: number;
605
+ scope?: string;
606
+ filter_rules?: Record<string, unknown>;
607
+ buyer_webhook_url?: string;
608
+ };
609
+
610
+ if (!Array.isArray(publisher_ids) || publisher_ids.length === 0) {
611
+ if (scope === "custom") {
612
+ return err("publisher_ids array is required for scope='custom'");
613
+ }
614
+ }
615
+ if (!pelEmail) return err("buyer_email is required");
616
+ if (!buyer_org) return err("buyer_org is required");
617
+
618
+ const body: Record<string, unknown> = {
619
+ publisher_ids: publisher_ids ?? [],
620
+ buyer_email: pelEmail,
621
+ buyer_org,
622
+ billing_type,
623
+ license_tier,
624
+ duration_months,
625
+ scope,
626
+ ...(filter_rules ? { filter_rules } : {}),
627
+ ...(buyer_webhook_url ? { buyer_webhook_url } : {}),
628
+ };
629
+
630
+ const data = await opeddFetch("/enterprise-license", {
631
+ method: "POST",
632
+ body: JSON.stringify(body),
633
+ });
634
+ return ok(data);
635
+ }
636
+
637
+ // ── list_feed (Phase 10 + 11) ──────────────────────────────────────────
638
+ case "list_feed": {
639
+ if (!ACCESS_KEY) {
640
+ return err("OPEDD_ACCESS_KEY env var is required for this tool (eak_* enterprise access key)");
641
+ }
642
+ const { since, cursor, limit = 50 } = args as {
643
+ since?: string;
644
+ cursor?: string;
645
+ limit?: number;
646
+ };
647
+ const params = new URLSearchParams({
648
+ access_key: ACCESS_KEY,
649
+ format: "json",
650
+ limit: String(Math.min(Number(limit) || 50, 200)),
651
+ });
652
+ if (since) params.set("since", since);
653
+ if (cursor) params.set("cursor", cursor);
654
+
655
+ const data = await opeddFetch(`/enterprise-license?${params.toString()}`);
656
+ return ok(data);
657
+ }
658
+
659
+ // ── stream_feed_ndjson (Phase 11 M3) ───────────────────────────────────
660
+ case "stream_feed_ndjson": {
661
+ if (!ACCESS_KEY) {
662
+ return err("OPEDD_ACCESS_KEY env var is required for this tool (eak_* enterprise access key)");
663
+ }
664
+ const { since, cursor, limit = 200 } = args as {
665
+ since?: string;
666
+ cursor?: string;
667
+ limit?: number;
668
+ };
669
+ const params = new URLSearchParams({
670
+ access_key: ACCESS_KEY,
671
+ format: "ndjson",
672
+ limit: String(Math.min(Number(limit) || 200, 1000)),
673
+ });
674
+ if (since) params.set("since", since);
675
+ if (cursor) params.set("cursor", cursor);
676
+
677
+ const data = await opeddFetchNdjson(`/enterprise-license?${params.toString()}`);
678
+ return ok(data);
679
+ }
680
+
681
+ // ── get_audit_events (Phase 9.x + 10 M5 attestation) ───────────────────
682
+ case "get_audit_events": {
683
+ if (!BUYER_JWT) {
684
+ return err("OPEDD_BUYER_JWT env var is required for this tool (Supabase session JWT)");
685
+ }
686
+ const { from, to, event_type, cursor, limit = 50 } = args as {
687
+ from?: string;
688
+ to?: string;
689
+ event_type?: string;
690
+ cursor?: string;
691
+ limit?: number;
692
+ };
693
+ const params = new URLSearchParams({
694
+ limit: String(Math.min(Number(limit) || 50, 200)),
695
+ });
696
+ if (from) params.set("from", from);
697
+ if (to) params.set("to", to);
698
+ if (event_type) params.set("event_type", event_type);
699
+ if (cursor) params.set("cursor", cursor);
700
+
701
+ const data = await opeddFetch(`/buyer-audit?${params.toString()}`, {
702
+ headers: { Authorization: `Bearer ${BUYER_JWT}` },
703
+ });
704
+ return ok(data);
705
+ }
706
+
707
+ // ── get_compliance_dossier (Phase 11 M4) ───────────────────────────────
708
+ case "get_compliance_dossier": {
709
+ if (!BUYER_JWT) {
710
+ return err("OPEDD_BUYER_JWT env var is required for this tool (Supabase session JWT)");
711
+ }
712
+ const { from, to, cursor } = args as {
713
+ from?: string;
714
+ to?: string;
715
+ cursor?: string;
716
+ };
717
+ if (!from) return err("from (ISO 8601 timestamp) is required");
718
+ if (!to) return err("to (ISO 8601 timestamp) is required");
719
+
720
+ const params = new URLSearchParams({
721
+ from,
722
+ to,
723
+ format: "json",
724
+ });
725
+ if (cursor) params.set("cursor", cursor);
726
+
727
+ const data = await opeddFetch(`/buyer-compliance-report?${params.toString()}`, {
728
+ headers: { Authorization: `Bearer ${BUYER_JWT}` },
729
+ });
730
+ return ok(data);
731
+ }
732
+
357
733
  // ── list_publisher_content ─────────────────────────────────────────────
358
734
  case "list_publisher_content": {
359
735
  if (!API_KEY) {