opedd-mcp 0.1.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/README.md +107 -0
- package/dist/index.js +255 -0
- package/package.json +36 -0
- package/src/index.ts +336 -0
- package/tsconfig.json +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# opedd-mcp
|
|
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.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
Exposes 4–5 tools to any AI assistant:
|
|
8
|
+
|
|
9
|
+
| Tool | Description |
|
|
10
|
+
|------|-------------|
|
|
11
|
+
| `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 |
|
|
13
|
+
| `verify_license` | Verify a license key — returns validity, article, publisher, blockchain proof |
|
|
14
|
+
| `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 |
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install -g opedd-mcp
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or run directly with npx:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx opedd-mcp
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Configuration
|
|
30
|
+
|
|
31
|
+
Set environment variables to pre-configure the server:
|
|
32
|
+
|
|
33
|
+
| Variable | Required | Description |
|
|
34
|
+
|----------|----------|-------------|
|
|
35
|
+
| `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 |
|
|
37
|
+
| `OPEDD_API_KEY` | Optional | Publisher API key (`op_...`) — enables `list_publisher_content` |
|
|
38
|
+
| `OPEDD_API_URL` | Optional | Override the API base URL (default: Opedd production) |
|
|
39
|
+
|
|
40
|
+
> **Getting a Stripe payment method ID**: Save a card in your Stripe account and retrieve the `pm_...` ID via the [Stripe API](https://stripe.com/docs/api/payment_methods).
|
|
41
|
+
|
|
42
|
+
## Claude Desktop setup
|
|
43
|
+
|
|
44
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"mcpServers": {
|
|
49
|
+
"opedd": {
|
|
50
|
+
"command": "npx",
|
|
51
|
+
"args": ["opedd-mcp"],
|
|
52
|
+
"env": {
|
|
53
|
+
"OPEDD_BUYER_EMAIL": "you@yourcompany.com",
|
|
54
|
+
"OPEDD_PAYMENT_METHOD_ID": "pm_..."
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Cursor / Windsurf setup
|
|
62
|
+
|
|
63
|
+
Add to your MCP settings:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"opedd": {
|
|
68
|
+
"command": "npx opedd-mcp",
|
|
69
|
+
"env": {
|
|
70
|
+
"OPEDD_BUYER_EMAIL": "you@yourcompany.com",
|
|
71
|
+
"OPEDD_PAYMENT_METHOD_ID": "pm_..."
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Example usage
|
|
78
|
+
|
|
79
|
+
Once configured, you can ask your AI assistant:
|
|
80
|
+
|
|
81
|
+
> "Look up the licensing options for this article: https://theinformation.com/articles/..."
|
|
82
|
+
|
|
83
|
+
> "Purchase a human republication license for that article on my behalf."
|
|
84
|
+
|
|
85
|
+
> "Verify license key OP-XXXX-XXXX and tell me what it covers."
|
|
86
|
+
|
|
87
|
+
> "Browse the latest licenses from The Information publisher."
|
|
88
|
+
|
|
89
|
+
The assistant will call the appropriate Opedd tools, show you the results, and—if you've pre-configured a payment method—can complete a purchase autonomously.
|
|
90
|
+
|
|
91
|
+
## Payment methods
|
|
92
|
+
|
|
93
|
+
Purchases use **Stripe** by default (via `pm_...` payment method IDs). USDC on Base is also supported by the Opedd API — pass `payment: { method: 'usdc', tx_hash: '0x...' }` directly to `agent-purchase` if building a custom integration.
|
|
94
|
+
|
|
95
|
+
## Development
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
git clone https://github.com/Opedd/opedd-mcp
|
|
99
|
+
cd opedd-mcp
|
|
100
|
+
npm install
|
|
101
|
+
npm run dev # runs with tsx (no build step)
|
|
102
|
+
npm run build # compiles to dist/
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
// ─── Configuration ────────────────────────────────────────────────────────────
|
|
6
|
+
const API_BASE = process.env.OPEDD_API_URL ??
|
|
7
|
+
"https://djdzcciayennqchjgybx.supabase.co/functions/v1";
|
|
8
|
+
const BUYER_EMAIL = process.env.OPEDD_BUYER_EMAIL;
|
|
9
|
+
const PAYMENT_METHOD_ID = process.env.OPEDD_PAYMENT_METHOD_ID;
|
|
10
|
+
const API_KEY = process.env.OPEDD_API_KEY; // publisher API key (op_...)
|
|
11
|
+
// ─── HTTP helpers ─────────────────────────────────────────────────────────────
|
|
12
|
+
async function opeddFetch(path, options = {}) {
|
|
13
|
+
const url = `${API_BASE}${path}`;
|
|
14
|
+
const headers = {
|
|
15
|
+
"Content-Type": "application/json",
|
|
16
|
+
...(API_KEY ? { "X-API-Key": API_KEY } : {}),
|
|
17
|
+
...(options.headers ?? {}),
|
|
18
|
+
};
|
|
19
|
+
const res = await fetch(url, { ...options, headers });
|
|
20
|
+
return res.json();
|
|
21
|
+
}
|
|
22
|
+
function ok(data) {
|
|
23
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
24
|
+
}
|
|
25
|
+
function err(msg) {
|
|
26
|
+
return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
|
|
27
|
+
}
|
|
28
|
+
// ─── Tool definitions ─────────────────────────────────────────────────────────
|
|
29
|
+
const TOOLS = [
|
|
30
|
+
{
|
|
31
|
+
name: "lookup_content",
|
|
32
|
+
description: "Look up a piece of content on the Opedd registry by URL. " +
|
|
33
|
+
"Returns the article title, publisher, available license types, and pricing " +
|
|
34
|
+
"(human republication price and AI training/inference price). " +
|
|
35
|
+
"Always call this first to check if content is licensable and what it costs.",
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: "object",
|
|
38
|
+
required: ["url"],
|
|
39
|
+
properties: {
|
|
40
|
+
url: {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "The canonical URL of the article or content to look up",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "purchase_license",
|
|
49
|
+
description: "Purchase a content license from the Opedd protocol using a Stripe payment method. " +
|
|
50
|
+
"Returns a license key (format: OP-XXXX-XXXX) and a certificate URL. " +
|
|
51
|
+
"The buyer receives a Handshake Email with their license key. " +
|
|
52
|
+
"Set OPEDD_BUYER_EMAIL and OPEDD_PAYMENT_METHOD_ID env vars to avoid passing them on every call. " +
|
|
53
|
+
"License types: 'human' = republication rights, 'ai' = training dataset rights, 'ai_inference' = inference/RAG rights.",
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: "object",
|
|
56
|
+
required: ["license_type"],
|
|
57
|
+
properties: {
|
|
58
|
+
article_url: {
|
|
59
|
+
type: "string",
|
|
60
|
+
description: "URL of the article to license (use this OR article_id)",
|
|
61
|
+
},
|
|
62
|
+
article_id: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "Opedd article UUID (use this OR article_url)",
|
|
65
|
+
},
|
|
66
|
+
license_type: {
|
|
67
|
+
type: "string",
|
|
68
|
+
enum: ["human", "ai", "ai_inference"],
|
|
69
|
+
description: "human = republication/editorial rights, ai = training dataset rights, ai_inference = inference/RAG rights",
|
|
70
|
+
},
|
|
71
|
+
buyer_email: {
|
|
72
|
+
type: "string",
|
|
73
|
+
description: "Email address for the license. Falls back to OPEDD_BUYER_EMAIL env var.",
|
|
74
|
+
},
|
|
75
|
+
buyer_name: {
|
|
76
|
+
type: "string",
|
|
77
|
+
description: "Full name of the buyer (for the license record and certificate)",
|
|
78
|
+
},
|
|
79
|
+
buyer_organization: {
|
|
80
|
+
type: "string",
|
|
81
|
+
description: "Organization or company name (for enterprise/editorial licenses)",
|
|
82
|
+
},
|
|
83
|
+
intended_use: {
|
|
84
|
+
type: "string",
|
|
85
|
+
enum: ["personal", "editorial", "commercial", "ai_training", "corporate"],
|
|
86
|
+
description: "Intended use of the licensed content",
|
|
87
|
+
},
|
|
88
|
+
payment_method_id: {
|
|
89
|
+
type: "string",
|
|
90
|
+
description: "Stripe payment method ID (pm_...). Falls back to OPEDD_PAYMENT_METHOD_ID env var.",
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "verify_license",
|
|
97
|
+
description: "Verify the authenticity of an Opedd license key. " +
|
|
98
|
+
"Returns license details including: article title, publisher, license type, " +
|
|
99
|
+
"issue date, amount paid, buyer info, and blockchain proof status. " +
|
|
100
|
+
"Use this to confirm a license is valid before using licensed content.",
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: "object",
|
|
103
|
+
required: ["license_key"],
|
|
104
|
+
properties: {
|
|
105
|
+
license_key: {
|
|
106
|
+
type: "string",
|
|
107
|
+
description: "The license key to verify (format: OP-XXXX-XXXX)",
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "browse_registry",
|
|
114
|
+
description: "Browse the public Opedd license registry. " +
|
|
115
|
+
"Returns recently issued licenses and licensable content. " +
|
|
116
|
+
"Filter by publisher_id to explore all content from a specific publisher. " +
|
|
117
|
+
"Filter by article_id to see all licenses issued for a specific article.",
|
|
118
|
+
inputSchema: {
|
|
119
|
+
type: "object",
|
|
120
|
+
properties: {
|
|
121
|
+
publisher_id: {
|
|
122
|
+
type: "string",
|
|
123
|
+
description: "Filter results to a specific publisher (UUID)",
|
|
124
|
+
},
|
|
125
|
+
article_id: {
|
|
126
|
+
type: "string",
|
|
127
|
+
description: "Filter results to a specific article (UUID)",
|
|
128
|
+
},
|
|
129
|
+
limit: {
|
|
130
|
+
type: "number",
|
|
131
|
+
description: "Number of results to return (default: 10, max: 50)",
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
// If a publisher API key is configured, expose publisher-specific tooling
|
|
138
|
+
if (API_KEY) {
|
|
139
|
+
TOOLS.push({
|
|
140
|
+
name: "list_publisher_content",
|
|
141
|
+
description: "List all licensable articles for the authenticated publisher (requires OPEDD_API_KEY). " +
|
|
142
|
+
"Returns articles with titles, descriptions, pricing, and sales statistics. " +
|
|
143
|
+
"Use article IDs from this list to purchase licenses via purchase_license.",
|
|
144
|
+
inputSchema: {
|
|
145
|
+
type: "object",
|
|
146
|
+
properties: {
|
|
147
|
+
limit: {
|
|
148
|
+
type: "number",
|
|
149
|
+
description: "Number of results (default: 20, max: 100)",
|
|
150
|
+
},
|
|
151
|
+
type: {
|
|
152
|
+
type: "string",
|
|
153
|
+
enum: ["human", "ai"],
|
|
154
|
+
description: "Filter by license type availability",
|
|
155
|
+
},
|
|
156
|
+
offset: {
|
|
157
|
+
type: "number",
|
|
158
|
+
description: "Pagination offset (default: 0)",
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
// ─── MCP Server ───────────────────────────────────────────────────────────────
|
|
165
|
+
const server = new Server({ name: "opedd-mcp", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
166
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
167
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
168
|
+
const { name, arguments: args = {} } = request.params;
|
|
169
|
+
try {
|
|
170
|
+
switch (name) {
|
|
171
|
+
// ── lookup_content ─────────────────────────────────────────────────────
|
|
172
|
+
case "lookup_content": {
|
|
173
|
+
const { url } = args;
|
|
174
|
+
if (!url)
|
|
175
|
+
return err("url is required");
|
|
176
|
+
const data = await opeddFetch(`/lookup-article?url=${encodeURIComponent(url)}`);
|
|
177
|
+
return ok(data);
|
|
178
|
+
}
|
|
179
|
+
// ── purchase_license ───────────────────────────────────────────────────
|
|
180
|
+
case "purchase_license": {
|
|
181
|
+
const { article_url, article_id, license_type, buyer_email: argEmail, buyer_name, buyer_organization, intended_use, payment_method_id: argPm, } = args;
|
|
182
|
+
if (!article_url && !article_id) {
|
|
183
|
+
return err("Either article_url or article_id is required");
|
|
184
|
+
}
|
|
185
|
+
const buyerEmail = argEmail || BUYER_EMAIL;
|
|
186
|
+
if (!buyerEmail) {
|
|
187
|
+
return err("buyer_email is required (or set the OPEDD_BUYER_EMAIL env var)");
|
|
188
|
+
}
|
|
189
|
+
const paymentMethodId = argPm || PAYMENT_METHOD_ID;
|
|
190
|
+
if (!paymentMethodId) {
|
|
191
|
+
return err("payment_method_id is required (or set the OPEDD_PAYMENT_METHOD_ID env var). " +
|
|
192
|
+
"Get a Stripe payment method ID by saving a card at stripe.com/docs/api/payment_methods.");
|
|
193
|
+
}
|
|
194
|
+
const body = {
|
|
195
|
+
license_type,
|
|
196
|
+
buyer_email: buyerEmail,
|
|
197
|
+
payment: { method: "stripe_pm", payment_method_id: paymentMethodId },
|
|
198
|
+
...(article_id ? { article_id } : { article_url }),
|
|
199
|
+
...(buyer_name ? { buyer_name } : {}),
|
|
200
|
+
...(buyer_organization ? { buyer_organization } : {}),
|
|
201
|
+
...(intended_use ? { intended_use } : {}),
|
|
202
|
+
};
|
|
203
|
+
const data = await opeddFetch("/agent-purchase", {
|
|
204
|
+
method: "POST",
|
|
205
|
+
body: JSON.stringify(body),
|
|
206
|
+
});
|
|
207
|
+
return ok(data);
|
|
208
|
+
}
|
|
209
|
+
// ── verify_license ─────────────────────────────────────────────────────
|
|
210
|
+
case "verify_license": {
|
|
211
|
+
const { license_key } = args;
|
|
212
|
+
if (!license_key)
|
|
213
|
+
return err("license_key is required");
|
|
214
|
+
const data = await opeddFetch(`/verify-license?key=${encodeURIComponent(license_key)}`);
|
|
215
|
+
return ok(data);
|
|
216
|
+
}
|
|
217
|
+
// ── browse_registry ────────────────────────────────────────────────────
|
|
218
|
+
case "browse_registry": {
|
|
219
|
+
const { publisher_id, article_id, limit = 10 } = args;
|
|
220
|
+
const params = new URLSearchParams();
|
|
221
|
+
if (publisher_id)
|
|
222
|
+
params.set("publisher_id", publisher_id);
|
|
223
|
+
if (article_id)
|
|
224
|
+
params.set("article_id", article_id);
|
|
225
|
+
params.set("limit", String(Math.min(Number(limit), 50)));
|
|
226
|
+
const data = await opeddFetch(`/registry?${params.toString()}`);
|
|
227
|
+
return ok(data);
|
|
228
|
+
}
|
|
229
|
+
// ── list_publisher_content ─────────────────────────────────────────────
|
|
230
|
+
case "list_publisher_content": {
|
|
231
|
+
if (!API_KEY) {
|
|
232
|
+
return err("OPEDD_API_KEY env var is required for this tool");
|
|
233
|
+
}
|
|
234
|
+
const { limit = 20, type, offset = 0 } = args;
|
|
235
|
+
const params = new URLSearchParams({ action: "articles" });
|
|
236
|
+
params.set("limit", String(Math.min(Number(limit), 100)));
|
|
237
|
+
params.set("offset", String(Number(offset)));
|
|
238
|
+
if (type)
|
|
239
|
+
params.set("type", type);
|
|
240
|
+
const data = await opeddFetch(`/api?${params.toString()}`);
|
|
241
|
+
return ok(data);
|
|
242
|
+
}
|
|
243
|
+
default:
|
|
244
|
+
return err(`Unknown tool: ${name}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (e) {
|
|
248
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
249
|
+
return err(`Request failed: ${msg}`);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
// ─── Start ────────────────────────────────────────────────────────────────────
|
|
253
|
+
const transport = new StdioServerTransport();
|
|
254
|
+
await server.connect(transport);
|
|
255
|
+
console.error("[opedd-mcp] Server running on stdio");
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opedd-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Opedd — discover, purchase, and verify content licenses from AI assistants",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"opedd-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"dev": "tsx src/index.ts"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"opedd",
|
|
18
|
+
"licensing",
|
|
19
|
+
"content",
|
|
20
|
+
"ai",
|
|
21
|
+
"model-context-protocol"
|
|
22
|
+
],
|
|
23
|
+
"author": "Opedd",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.0.0",
|
|
30
|
+
"tsx": "^4.0.0",
|
|
31
|
+
"typescript": "^5.0.0"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import {
|
|
6
|
+
CallToolRequestSchema,
|
|
7
|
+
ListToolsRequestSchema,
|
|
8
|
+
type Tool,
|
|
9
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
|
|
11
|
+
// ─── Configuration ────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
const API_BASE =
|
|
14
|
+
process.env.OPEDD_API_URL ??
|
|
15
|
+
"https://djdzcciayennqchjgybx.supabase.co/functions/v1";
|
|
16
|
+
|
|
17
|
+
const BUYER_EMAIL = process.env.OPEDD_BUYER_EMAIL;
|
|
18
|
+
const PAYMENT_METHOD_ID = process.env.OPEDD_PAYMENT_METHOD_ID;
|
|
19
|
+
const API_KEY = process.env.OPEDD_API_KEY; // publisher API key (op_...)
|
|
20
|
+
|
|
21
|
+
// ─── HTTP helpers ─────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
async function opeddFetch(path: string, options: RequestInit = {}): Promise<unknown> {
|
|
24
|
+
const url = `${API_BASE}${path}`;
|
|
25
|
+
const headers: Record<string, string> = {
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
...(API_KEY ? { "X-API-Key": API_KEY } : {}),
|
|
28
|
+
...(options.headers as Record<string, string> ?? {}),
|
|
29
|
+
};
|
|
30
|
+
const res = await fetch(url, { ...options, headers });
|
|
31
|
+
return res.json();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── MCP response helpers ─────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
type TextContent = { type: "text"; text: string };
|
|
37
|
+
type ToolResult = { content: TextContent[]; isError?: boolean };
|
|
38
|
+
|
|
39
|
+
function ok(data: unknown): ToolResult {
|
|
40
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function err(msg: string): ToolResult {
|
|
44
|
+
return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── Tool definitions ─────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
const TOOLS: Tool[] = [
|
|
50
|
+
{
|
|
51
|
+
name: "lookup_content",
|
|
52
|
+
description:
|
|
53
|
+
"Look up a piece of content on the Opedd registry by URL. " +
|
|
54
|
+
"Returns the article title, publisher, available license types, and pricing " +
|
|
55
|
+
"(human republication price and AI training/inference price). " +
|
|
56
|
+
"Always call this first to check if content is licensable and what it costs.",
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: "object",
|
|
59
|
+
required: ["url"],
|
|
60
|
+
properties: {
|
|
61
|
+
url: {
|
|
62
|
+
type: "string",
|
|
63
|
+
description: "The canonical URL of the article or content to look up",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "purchase_license",
|
|
70
|
+
description:
|
|
71
|
+
"Purchase a content license from the Opedd protocol using a Stripe payment method. " +
|
|
72
|
+
"Returns a license key (format: OP-XXXX-XXXX) and a certificate URL. " +
|
|
73
|
+
"The buyer receives a Handshake Email with their license key. " +
|
|
74
|
+
"Set OPEDD_BUYER_EMAIL and OPEDD_PAYMENT_METHOD_ID env vars to avoid passing them on every call. " +
|
|
75
|
+
"License types: 'human' = republication rights, 'ai' = training dataset rights, 'ai_inference' = inference/RAG rights.",
|
|
76
|
+
inputSchema: {
|
|
77
|
+
type: "object",
|
|
78
|
+
required: ["license_type"],
|
|
79
|
+
properties: {
|
|
80
|
+
article_url: {
|
|
81
|
+
type: "string",
|
|
82
|
+
description: "URL of the article to license (use this OR article_id)",
|
|
83
|
+
},
|
|
84
|
+
article_id: {
|
|
85
|
+
type: "string",
|
|
86
|
+
description: "Opedd article UUID (use this OR article_url)",
|
|
87
|
+
},
|
|
88
|
+
license_type: {
|
|
89
|
+
type: "string",
|
|
90
|
+
enum: ["human", "ai", "ai_inference"],
|
|
91
|
+
description:
|
|
92
|
+
"human = republication/editorial rights, ai = training dataset rights, ai_inference = inference/RAG rights",
|
|
93
|
+
},
|
|
94
|
+
buyer_email: {
|
|
95
|
+
type: "string",
|
|
96
|
+
description: "Email address for the license. Falls back to OPEDD_BUYER_EMAIL env var.",
|
|
97
|
+
},
|
|
98
|
+
buyer_name: {
|
|
99
|
+
type: "string",
|
|
100
|
+
description: "Full name of the buyer (for the license record and certificate)",
|
|
101
|
+
},
|
|
102
|
+
buyer_organization: {
|
|
103
|
+
type: "string",
|
|
104
|
+
description: "Organization or company name (for enterprise/editorial licenses)",
|
|
105
|
+
},
|
|
106
|
+
intended_use: {
|
|
107
|
+
type: "string",
|
|
108
|
+
enum: ["personal", "editorial", "commercial", "ai_training", "corporate"],
|
|
109
|
+
description: "Intended use of the licensed content",
|
|
110
|
+
},
|
|
111
|
+
payment_method_id: {
|
|
112
|
+
type: "string",
|
|
113
|
+
description:
|
|
114
|
+
"Stripe payment method ID (pm_...). Falls back to OPEDD_PAYMENT_METHOD_ID env var.",
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "verify_license",
|
|
121
|
+
description:
|
|
122
|
+
"Verify the authenticity of an Opedd license key. " +
|
|
123
|
+
"Returns license details including: article title, publisher, license type, " +
|
|
124
|
+
"issue date, amount paid, buyer info, and blockchain proof status. " +
|
|
125
|
+
"Use this to confirm a license is valid before using licensed content.",
|
|
126
|
+
inputSchema: {
|
|
127
|
+
type: "object",
|
|
128
|
+
required: ["license_key"],
|
|
129
|
+
properties: {
|
|
130
|
+
license_key: {
|
|
131
|
+
type: "string",
|
|
132
|
+
description: "The license key to verify (format: OP-XXXX-XXXX)",
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "browse_registry",
|
|
139
|
+
description:
|
|
140
|
+
"Browse the public Opedd license registry. " +
|
|
141
|
+
"Returns recently issued licenses and licensable content. " +
|
|
142
|
+
"Filter by publisher_id to explore all content from a specific publisher. " +
|
|
143
|
+
"Filter by article_id to see all licenses issued for a specific article.",
|
|
144
|
+
inputSchema: {
|
|
145
|
+
type: "object",
|
|
146
|
+
properties: {
|
|
147
|
+
publisher_id: {
|
|
148
|
+
type: "string",
|
|
149
|
+
description: "Filter results to a specific publisher (UUID)",
|
|
150
|
+
},
|
|
151
|
+
article_id: {
|
|
152
|
+
type: "string",
|
|
153
|
+
description: "Filter results to a specific article (UUID)",
|
|
154
|
+
},
|
|
155
|
+
limit: {
|
|
156
|
+
type: "number",
|
|
157
|
+
description: "Number of results to return (default: 10, max: 50)",
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
// If a publisher API key is configured, expose publisher-specific tooling
|
|
165
|
+
if (API_KEY) {
|
|
166
|
+
TOOLS.push({
|
|
167
|
+
name: "list_publisher_content",
|
|
168
|
+
description:
|
|
169
|
+
"List all licensable articles for the authenticated publisher (requires OPEDD_API_KEY). " +
|
|
170
|
+
"Returns articles with titles, descriptions, pricing, and sales statistics. " +
|
|
171
|
+
"Use article IDs from this list to purchase licenses via purchase_license.",
|
|
172
|
+
inputSchema: {
|
|
173
|
+
type: "object",
|
|
174
|
+
properties: {
|
|
175
|
+
limit: {
|
|
176
|
+
type: "number",
|
|
177
|
+
description: "Number of results (default: 20, max: 100)",
|
|
178
|
+
},
|
|
179
|
+
type: {
|
|
180
|
+
type: "string",
|
|
181
|
+
enum: ["human", "ai"],
|
|
182
|
+
description: "Filter by license type availability",
|
|
183
|
+
},
|
|
184
|
+
offset: {
|
|
185
|
+
type: "number",
|
|
186
|
+
description: "Pagination offset (default: 0)",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ─── MCP Server ───────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
const server = new Server(
|
|
196
|
+
{ name: "opedd-mcp", version: "0.1.0" },
|
|
197
|
+
{ capabilities: { tools: {} } }
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
201
|
+
|
|
202
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
203
|
+
const { name, arguments: args = {} } = request.params;
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
switch (name) {
|
|
207
|
+
// ── lookup_content ─────────────────────────────────────────────────────
|
|
208
|
+
case "lookup_content": {
|
|
209
|
+
const { url } = args as { url: string };
|
|
210
|
+
if (!url) return err("url is required");
|
|
211
|
+
|
|
212
|
+
const data = await opeddFetch(
|
|
213
|
+
`/lookup-article?url=${encodeURIComponent(url)}`
|
|
214
|
+
);
|
|
215
|
+
return ok(data);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── purchase_license ───────────────────────────────────────────────────
|
|
219
|
+
case "purchase_license": {
|
|
220
|
+
const {
|
|
221
|
+
article_url,
|
|
222
|
+
article_id,
|
|
223
|
+
license_type,
|
|
224
|
+
buyer_email: argEmail,
|
|
225
|
+
buyer_name,
|
|
226
|
+
buyer_organization,
|
|
227
|
+
intended_use,
|
|
228
|
+
payment_method_id: argPm,
|
|
229
|
+
} = args as {
|
|
230
|
+
article_url?: string;
|
|
231
|
+
article_id?: string;
|
|
232
|
+
license_type: string;
|
|
233
|
+
buyer_email?: string;
|
|
234
|
+
buyer_name?: string;
|
|
235
|
+
buyer_organization?: string;
|
|
236
|
+
intended_use?: string;
|
|
237
|
+
payment_method_id?: string;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
if (!article_url && !article_id) {
|
|
241
|
+
return err("Either article_url or article_id is required");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const buyerEmail = argEmail || BUYER_EMAIL;
|
|
245
|
+
if (!buyerEmail) {
|
|
246
|
+
return err("buyer_email is required (or set the OPEDD_BUYER_EMAIL env var)");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const paymentMethodId = argPm || PAYMENT_METHOD_ID;
|
|
250
|
+
if (!paymentMethodId) {
|
|
251
|
+
return err(
|
|
252
|
+
"payment_method_id is required (or set the OPEDD_PAYMENT_METHOD_ID env var). " +
|
|
253
|
+
"Get a Stripe payment method ID by saving a card at stripe.com/docs/api/payment_methods."
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const body: Record<string, unknown> = {
|
|
258
|
+
license_type,
|
|
259
|
+
buyer_email: buyerEmail,
|
|
260
|
+
payment: { method: "stripe_pm", payment_method_id: paymentMethodId },
|
|
261
|
+
...(article_id ? { article_id } : { article_url }),
|
|
262
|
+
...(buyer_name ? { buyer_name } : {}),
|
|
263
|
+
...(buyer_organization ? { buyer_organization } : {}),
|
|
264
|
+
...(intended_use ? { intended_use } : {}),
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const data = await opeddFetch("/agent-purchase", {
|
|
268
|
+
method: "POST",
|
|
269
|
+
body: JSON.stringify(body),
|
|
270
|
+
});
|
|
271
|
+
return ok(data);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── verify_license ─────────────────────────────────────────────────────
|
|
275
|
+
case "verify_license": {
|
|
276
|
+
const { license_key } = args as { license_key: string };
|
|
277
|
+
if (!license_key) return err("license_key is required");
|
|
278
|
+
|
|
279
|
+
const data = await opeddFetch(
|
|
280
|
+
`/verify-license?key=${encodeURIComponent(license_key)}`
|
|
281
|
+
);
|
|
282
|
+
return ok(data);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ── browse_registry ────────────────────────────────────────────────────
|
|
286
|
+
case "browse_registry": {
|
|
287
|
+
const { publisher_id, article_id, limit = 10 } = args as {
|
|
288
|
+
publisher_id?: string;
|
|
289
|
+
article_id?: string;
|
|
290
|
+
limit?: number;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const params = new URLSearchParams();
|
|
294
|
+
if (publisher_id) params.set("publisher_id", publisher_id);
|
|
295
|
+
if (article_id) params.set("article_id", article_id);
|
|
296
|
+
params.set("limit", String(Math.min(Number(limit), 50)));
|
|
297
|
+
|
|
298
|
+
const data = await opeddFetch(`/registry?${params.toString()}`);
|
|
299
|
+
return ok(data);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── list_publisher_content ─────────────────────────────────────────────
|
|
303
|
+
case "list_publisher_content": {
|
|
304
|
+
if (!API_KEY) {
|
|
305
|
+
return err("OPEDD_API_KEY env var is required for this tool");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const { limit = 20, type, offset = 0 } = args as {
|
|
309
|
+
limit?: number;
|
|
310
|
+
type?: string;
|
|
311
|
+
offset?: number;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const params = new URLSearchParams({ action: "articles" });
|
|
315
|
+
params.set("limit", String(Math.min(Number(limit), 100)));
|
|
316
|
+
params.set("offset", String(Number(offset)));
|
|
317
|
+
if (type) params.set("type", type);
|
|
318
|
+
|
|
319
|
+
const data = await opeddFetch(`/api?${params.toString()}`);
|
|
320
|
+
return ok(data);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
default:
|
|
324
|
+
return err(`Unknown tool: ${name}`);
|
|
325
|
+
}
|
|
326
|
+
} catch (e) {
|
|
327
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
328
|
+
return err(`Request failed: ${msg}`);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// ─── Start ────────────────────────────────────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
const transport = new StdioServerTransport();
|
|
335
|
+
await server.connect(transport);
|
|
336
|
+
console.error("[opedd-mcp] Server running on stdio");
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"resolveJsonModule": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"],
|
|
14
|
+
"exclude": ["node_modules", "dist"]
|
|
15
|
+
}
|