opedd-mcp 0.4.0 → 0.5.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 +19 -0
- package/dist/index.js +35 -8
- package/dist/telemetry.js +89 -0
- package/package.json +9 -4
- package/.github/workflows/ci.yml +0 -70
- package/src/index.ts +0 -1034
- package/tests/dispatcher.env-gated.test.ts +0 -283
- package/tests/dispatcher.test.ts +0 -419
- package/tsconfig.json +0 -15
package/README.md
CHANGED
|
@@ -87,9 +87,28 @@ Set environment variables to pre-configure the server:
|
|
|
87
87
|
| `OPEDD_PUB_BEARER` | Optional | Canonical Publisher API Bearer key (`opedd_pub_<env>_<32-hex>`; issued via `POST /publishers-api-keys action=create_api_key`) — enables `list_publisher_content`. **v0.4.0 canonical.** |
|
|
88
88
|
| `OPEDD_API_KEY` | Deprecated | Legacy Publisher API key (`op_...`) — fallback during the transition window; will stop working when opedd-backend Phase C deploys. Migrate to `OPEDD_PUB_BEARER`. |
|
|
89
89
|
| `OPEDD_API_URL` | Optional | Override the API base URL (default: Opedd production) |
|
|
90
|
+
| `OPEDD_MCP_TELEMETRY` | Optional | Set to `0` to disable anonymous usage telemetry (see below) |
|
|
90
91
|
|
|
91
92
|
> **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).
|
|
92
93
|
|
|
94
|
+
## Anonymous usage telemetry
|
|
95
|
+
|
|
96
|
+
To understand how AI assistants use this server, `opedd-mcp` sends anonymous
|
|
97
|
+
telemetry for each tool call: **only the tool name, its duration, whether it
|
|
98
|
+
succeeded, and the server version** — plus a random per-process id. It never
|
|
99
|
+
sends tool parameters, responses, your email, API keys, tokens, content, or
|
|
100
|
+
your IP. Data goes to Opedd's PostHog (EU region).
|
|
101
|
+
|
|
102
|
+
**To opt out**, set either environment variable before starting the server:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
OPEDD_MCP_TELEMETRY=0
|
|
106
|
+
# or the cross-tool standard:
|
|
107
|
+
DO_NOT_TRACK=1
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
When opted out, no telemetry client is created and nothing is sent.
|
|
111
|
+
|
|
93
112
|
## Claude Desktop setup
|
|
94
113
|
|
|
95
114
|
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
package/dist/index.js
CHANGED
|
@@ -3,12 +3,22 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { initTelemetry, captureToolCall, shutdownTelemetry } from "./telemetry.js";
|
|
6
7
|
// ─── Configuration ────────────────────────────────────────────────────────────
|
|
7
8
|
const API_BASE = process.env.OPEDD_API_URL ??
|
|
8
9
|
"https://api.opedd.com";
|
|
9
10
|
const BUYER_EMAIL = process.env.OPEDD_BUYER_EMAIL;
|
|
10
11
|
const PAYMENT_METHOD_ID = process.env.OPEDD_PAYMENT_METHOD_ID;
|
|
11
|
-
|
|
12
|
+
// B91 v0.4.0 migration (2026-05-26): canonical Bearer auth.
|
|
13
|
+
// OPEDD_PUB_BEARER is the new canonical opedd_pub_<env>_<32-hex> key
|
|
14
|
+
// (issued via POST /publishers-api-keys action=create_api_key).
|
|
15
|
+
// OPEDD_API_KEY retained as backward-compat alias for legacy op_ keys
|
|
16
|
+
// during the dual-mode transition window on the /api endpoint (Phase A
|
|
17
|
+
// shipped opedd-backend 2026-05-26). Backend Phase C will drop the
|
|
18
|
+
// op_ path entirely; users MUST migrate to OPEDD_PUB_BEARER before
|
|
19
|
+
// then. Prefer Bearer if both set.
|
|
20
|
+
const PUB_BEARER = process.env.OPEDD_PUB_BEARER; // canonical opedd_pub_<env>_<32-hex>
|
|
21
|
+
const API_KEY = process.env.OPEDD_API_KEY; // LEGACY op_<32-hex> — retiring per backend Phase C
|
|
12
22
|
const BUYER_TOKEN = process.env.OPEDD_BUYER_TOKEN; // buyer API token (opedd_buyer_live_... or opedd_buyer_test_...)
|
|
13
23
|
const ACCESS_KEY = process.env.OPEDD_ACCESS_KEY; // enterprise access key (ent_*); for /enterprise-license GET feed
|
|
14
24
|
const BUYER_JWT = process.env.OPEDD_BUYER_JWT; // Supabase JWT; for /buyer-audit + /buyer-compliance-report
|
|
@@ -17,7 +27,9 @@ async function opeddFetch(path, options = {}) {
|
|
|
17
27
|
const url = `${API_BASE}${path}`;
|
|
18
28
|
const headers = {
|
|
19
29
|
"Content-Type": "application/json",
|
|
20
|
-
|
|
30
|
+
// B91 v0.4.0 (2026-05-26): canonical Bearer preferred; legacy X-API-Key
|
|
31
|
+
// accepted only as fallback during transition window
|
|
32
|
+
...(PUB_BEARER ? { Authorization: `Bearer ${PUB_BEARER}` } : (API_KEY ? { "X-API-Key": API_KEY } : {})),
|
|
21
33
|
...(options.headers ?? {}),
|
|
22
34
|
};
|
|
23
35
|
const res = await fetch(url, { ...options, headers });
|
|
@@ -35,7 +47,8 @@ async function opeddFetchNdjson(path, options = {}) {
|
|
|
35
47
|
const url = `${API_BASE}${path}`;
|
|
36
48
|
const headers = {
|
|
37
49
|
Accept: "application/x-ndjson",
|
|
38
|
-
|
|
50
|
+
// B91 v0.4.0 (2026-05-26): canonical Bearer preferred; legacy fallback
|
|
51
|
+
...(PUB_BEARER ? { Authorization: `Bearer ${PUB_BEARER}` } : (API_KEY ? { "X-API-Key": API_KEY } : {})),
|
|
39
52
|
...(options.headers ?? {}),
|
|
40
53
|
};
|
|
41
54
|
const res = await fetch(url, { ...options, headers });
|
|
@@ -527,10 +540,10 @@ if (BUYER_JWT) {
|
|
|
527
540
|
});
|
|
528
541
|
}
|
|
529
542
|
// If a publisher API key is configured, expose publisher-specific tooling
|
|
530
|
-
if (API_KEY) {
|
|
543
|
+
if (PUB_BEARER || API_KEY) {
|
|
531
544
|
TOOLS.push({
|
|
532
545
|
name: "list_publisher_content",
|
|
533
|
-
description: "List all licensable articles for the authenticated publisher (requires OPEDD_API_KEY). " +
|
|
546
|
+
description: "List all licensable articles for the authenticated publisher (requires OPEDD_PUB_BEARER, or legacy OPEDD_API_KEY). " +
|
|
534
547
|
"Returns articles with titles, descriptions, pricing, and sales statistics. " +
|
|
535
548
|
"Use article IDs from this list to purchase licenses via purchase_license.",
|
|
536
549
|
inputSchema: {
|
|
@@ -556,7 +569,15 @@ if (API_KEY) {
|
|
|
556
569
|
// ─── MCP Server ───────────────────────────────────────────────────────────────
|
|
557
570
|
const server = new Server({ name: "opedd-mcp", version: "0.3.0" }, { capabilities: { tools: {} } });
|
|
558
571
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
559
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) =>
|
|
572
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
573
|
+
// Anonymous usage telemetry — captures only the tool name, duration, and
|
|
574
|
+
// success flag (see telemetry.ts). Wrapping the runtime handler (not
|
|
575
|
+
// dispatchTool) keeps unit tests, which call dispatchTool directly, silent.
|
|
576
|
+
const start = Date.now();
|
|
577
|
+
const result = await dispatchTool(request.params.name, (request.params.arguments ?? {}));
|
|
578
|
+
captureToolCall(request.params.name, Date.now() - start, result.isError !== true);
|
|
579
|
+
return result;
|
|
580
|
+
});
|
|
560
581
|
// ─── Tool dispatcher (exported for unit tests) ────────────────────────────────
|
|
561
582
|
//
|
|
562
583
|
// Extracted from the inline handler 2026-05-24 EEST as part of the opedd-mcp
|
|
@@ -815,8 +836,8 @@ export async function dispatchTool(name, args) {
|
|
|
815
836
|
}
|
|
816
837
|
// ── list_publisher_content ─────────────────────────────────────────────
|
|
817
838
|
case "list_publisher_content": {
|
|
818
|
-
if (!API_KEY) {
|
|
819
|
-
return err("
|
|
839
|
+
if (!PUB_BEARER && !API_KEY) {
|
|
840
|
+
return err("OPEDD_PUB_BEARER env var is required for this tool (canonical Bearer; legacy OPEDD_API_KEY also accepted during transition)");
|
|
820
841
|
}
|
|
821
842
|
const { limit = 20, type, offset = 0 } = args;
|
|
822
843
|
const params = new URLSearchParams({ action: "articles" });
|
|
@@ -844,6 +865,12 @@ export async function dispatchTool(name, args) {
|
|
|
844
865
|
if (typeof process !== "undefined" &&
|
|
845
866
|
process.argv?.[1] &&
|
|
846
867
|
import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
868
|
+
initTelemetry();
|
|
869
|
+
for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
870
|
+
process.on(sig, () => {
|
|
871
|
+
void shutdownTelemetry().finally(() => process.exit(0));
|
|
872
|
+
});
|
|
873
|
+
}
|
|
847
874
|
const transport = new StdioServerTransport();
|
|
848
875
|
await server.connect(transport);
|
|
849
876
|
console.error("[opedd-mcp] Server running on stdio");
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Anonymous usage telemetry for the Opedd MCP server.
|
|
2
|
+
//
|
|
3
|
+
// Captures ONLY which tool an agent called, how long it took, and whether it
|
|
4
|
+
// succeeded — NEVER parameters, responses, PII, keys, or content. This module
|
|
5
|
+
// is only ever handed the tool name + duration + ok flag, so there is nothing
|
|
6
|
+
// sensitive to leak by construction. It reports to Opedd's PostHog (EU region)
|
|
7
|
+
// so we can understand how AI agents use the MCP distribution channel.
|
|
8
|
+
//
|
|
9
|
+
// Opt out: set OPEDD_MCP_TELEMETRY=0 (or "false"/"off"), or the widely-used
|
|
10
|
+
// DO_NOT_TRACK=1. When opted out no client is created and nothing is sent.
|
|
11
|
+
//
|
|
12
|
+
// stdio-safe: PostHog is reached over HTTPS only; this module NEVER writes to
|
|
13
|
+
// stdout (that would corrupt the MCP JSON-RPC stream). All errors are swallowed
|
|
14
|
+
// so telemetry can never affect a tool call or the server.
|
|
15
|
+
import { PostHog } from "posthog-node";
|
|
16
|
+
import { randomUUID } from "node:crypto";
|
|
17
|
+
// Public, write-only PostHog project key (EU cloud, project 218295). Safe to
|
|
18
|
+
// embed — it can only send events, not read data.
|
|
19
|
+
const POSTHOG_KEY = "phc_zrKQzZLUZikynMWJWMHUiQBgRsJa2fLzDFMdh6HxGU6c";
|
|
20
|
+
const POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
21
|
+
const SERVER_VERSION = "0.5.0";
|
|
22
|
+
/** Whether telemetry is allowed (respects OPEDD_MCP_TELEMETRY + DO_NOT_TRACK). Pure — exported for tests. */
|
|
23
|
+
export function isTelemetryEnabled() {
|
|
24
|
+
const t = process.env.OPEDD_MCP_TELEMETRY?.toLowerCase();
|
|
25
|
+
if (t === "0" || t === "false" || t === "off" || t === "no")
|
|
26
|
+
return false;
|
|
27
|
+
const dnt = process.env.DO_NOT_TRACK?.toLowerCase();
|
|
28
|
+
if (dnt === "1" || dnt === "true")
|
|
29
|
+
return false;
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The ONLY properties ever sent for a tool call — no parameters, responses,
|
|
34
|
+
* PII, keys, or content by construction. Pure; exported so a test can assert
|
|
35
|
+
* the privacy invariant directly.
|
|
36
|
+
*/
|
|
37
|
+
export function toolCallProperties(tool, durationMs, ok) {
|
|
38
|
+
return {
|
|
39
|
+
tool,
|
|
40
|
+
duration_ms: Math.round(durationMs),
|
|
41
|
+
ok,
|
|
42
|
+
server_version: SERVER_VERSION,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
let client = null;
|
|
46
|
+
// Anonymous per-process session id — not tied to any user, buyer, key, or IP.
|
|
47
|
+
const sessionId = randomUUID();
|
|
48
|
+
export function initTelemetry() {
|
|
49
|
+
if (!isTelemetryEnabled())
|
|
50
|
+
return;
|
|
51
|
+
try {
|
|
52
|
+
client = new PostHog(POSTHOG_KEY, {
|
|
53
|
+
host: POSTHOG_HOST,
|
|
54
|
+
flushAt: 1, // low-volume telemetry; deliver each event promptly
|
|
55
|
+
flushInterval: 30000,
|
|
56
|
+
});
|
|
57
|
+
// Swallow any transport error so it can never surface or crash the server.
|
|
58
|
+
client.on?.("error", () => { });
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
client = null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function captureToolCall(tool, durationMs, ok) {
|
|
65
|
+
if (!client)
|
|
66
|
+
return;
|
|
67
|
+
try {
|
|
68
|
+
client.capture({
|
|
69
|
+
distinctId: sessionId,
|
|
70
|
+
event: "mcp_tool_call",
|
|
71
|
+
properties: toolCallProperties(tool, durationMs, ok),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
/* telemetry must never affect a tool call */
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export async function shutdownTelemetry() {
|
|
79
|
+
const c = client;
|
|
80
|
+
client = null;
|
|
81
|
+
if (!c)
|
|
82
|
+
return;
|
|
83
|
+
try {
|
|
84
|
+
await c.shutdown();
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
/* ignore */
|
|
88
|
+
}
|
|
89
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opedd-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "MCP server for Opedd — discover, purchase, verify, and audit content licenses from AI assistants. Covers Phase 11 buyer-side surfaces + Phase 12 Wave 1 + 3 regulatory surfaces (CDSM Article 4(3) RSL + EU AI Act Article 53 attestation + platform onboarding helper).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"start": "node dist/index.js",
|
|
13
13
|
"dev": "tsx src/index.ts",
|
|
14
14
|
"test": "vitest run",
|
|
15
|
-
"test:watch": "vitest"
|
|
15
|
+
"test:watch": "vitest",
|
|
16
|
+
"prepublishOnly": "npm run build && npm test"
|
|
16
17
|
},
|
|
17
18
|
"keywords": [
|
|
18
19
|
"mcp",
|
|
@@ -25,7 +26,8 @@
|
|
|
25
26
|
"author": "Opedd",
|
|
26
27
|
"license": "MIT",
|
|
27
28
|
"dependencies": {
|
|
28
|
-
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
30
|
+
"posthog-node": "^4.0.0"
|
|
29
31
|
},
|
|
30
32
|
"devDependencies": {
|
|
31
33
|
"@types/node": "^22.19.19",
|
|
@@ -35,5 +37,8 @@
|
|
|
35
37
|
},
|
|
36
38
|
"engines": {
|
|
37
39
|
"node": ">=20"
|
|
38
|
-
}
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist"
|
|
43
|
+
]
|
|
39
44
|
}
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
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 18 dropped 2026-05-24 — vitest 4.x (rolldown dep) requires
|
|
16
|
-
# `node:util.styleText` which is only available on Node 21.7+.
|
|
17
|
-
# Node 18 reaches active LTS end April 2025; engines.node in
|
|
18
|
-
# package.json bumped to ">=20" in the same commit. Re-add Node 18
|
|
19
|
-
# only if vitest downgrade ratified (which is the alternative;
|
|
20
|
-
# founder picked engines bump).
|
|
21
|
-
node-version: ["20", "22"]
|
|
22
|
-
steps:
|
|
23
|
-
- uses: actions/checkout@v4
|
|
24
|
-
|
|
25
|
-
- name: Set up Node.js ${{ matrix.node-version }}
|
|
26
|
-
uses: actions/setup-node@v4
|
|
27
|
-
with:
|
|
28
|
-
node-version: ${{ matrix.node-version }}
|
|
29
|
-
cache: npm
|
|
30
|
-
|
|
31
|
-
- name: Install dependencies
|
|
32
|
-
# Using `npm install` rather than `npm ci` because the lockfile
|
|
33
|
-
# generated on macOS doesn't include Linux-conditional optional
|
|
34
|
-
# native deps (e.g. @emnapi/* used by vitest 4.x via napi-rs).
|
|
35
|
-
# `npm install` resolves transitively at install time per-platform;
|
|
36
|
-
# `npm ci` strictly validates lockfile-against-package.json and
|
|
37
|
-
# rejects when platform-specific transitive deps are missing.
|
|
38
|
-
# Trade-off: install is slower + slightly looser; acceptable for
|
|
39
|
-
# this repo's CI scale.
|
|
40
|
-
run: npm install
|
|
41
|
-
|
|
42
|
-
- name: TypeScript compile
|
|
43
|
-
run: npm run build
|
|
44
|
-
|
|
45
|
-
- name: Unit tests (vitest)
|
|
46
|
-
run: npm test
|
|
47
|
-
|
|
48
|
-
- name: Verify dist/index.js produced
|
|
49
|
-
run: |
|
|
50
|
-
test -f dist/index.js || (echo "::error::dist/index.js not produced by tsc"; exit 1)
|
|
51
|
-
echo "dist/index.js bytes: $(wc -c < dist/index.js)"
|
|
52
|
-
|
|
53
|
-
- name: Smoke-boot the MCP server
|
|
54
|
-
run: |
|
|
55
|
-
# Send initialize + tools/list, confirm v0.2.0 boots and lists tools
|
|
56
|
-
printf '%s\n%s\n' \
|
|
57
|
-
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"ci","version":"1"}}}' \
|
|
58
|
-
'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
|
|
59
|
-
| timeout 10 node dist/index.js > /tmp/mcp-output.txt 2> /tmp/mcp-stderr.txt || true
|
|
60
|
-
if ! grep -q '"serverInfo":{"name":"opedd-mcp"' /tmp/mcp-output.txt; then
|
|
61
|
-
echo "::error::MCP server did not respond with expected serverInfo"
|
|
62
|
-
cat /tmp/mcp-output.txt
|
|
63
|
-
cat /tmp/mcp-stderr.txt
|
|
64
|
-
exit 1
|
|
65
|
-
fi
|
|
66
|
-
if ! grep -q '"name":"lookup_content"' /tmp/mcp-output.txt; then
|
|
67
|
-
echo "::error::MCP server did not list lookup_content tool"
|
|
68
|
-
exit 1
|
|
69
|
-
fi
|
|
70
|
-
echo "MCP smoke boot OK"
|