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.
- package/.github/workflows/ci.yml +53 -0
- package/README.md +62 -5
- package/dist/index.js +336 -6
- package/package.json +2 -2
- package/src/index.ts +382 -6
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
| `
|
|
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://
|
|
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_
|
|
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.
|
|
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.
|
|
4
|
-
"description": "MCP server for Opedd — discover, purchase, and
|
|
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://
|
|
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_
|
|
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.
|
|
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) {
|