opedd-mcp 0.4.1 → 0.5.1

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 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,6 +3,7 @@ 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";
@@ -568,7 +569,15 @@ if (PUB_BEARER || API_KEY) {
568
569
  // ─── MCP Server ───────────────────────────────────────────────────────────────
569
570
  const server = new Server({ name: "opedd-mcp", version: "0.3.0" }, { capabilities: { tools: {} } });
570
571
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
571
- server.setRequestHandler(CallToolRequestSchema, async (request) => dispatchTool(request.params.name, (request.params.arguments ?? {})));
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
+ });
572
581
  // ─── Tool dispatcher (exported for unit tests) ────────────────────────────────
573
582
  //
574
583
  // Extracted from the inline handler 2026-05-24 EEST as part of the opedd-mcp
@@ -856,6 +865,12 @@ export async function dispatchTool(name, args) {
856
865
  if (typeof process !== "undefined" &&
857
866
  process.argv?.[1] &&
858
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
+ }
859
874
  const transport = new StdioServerTransport();
860
875
  await server.connect(transport);
861
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_yfyXBNsf5nZncZBiZNQBBs5RWC3mtWNWpUJv5EfzfBir";
20
+ const POSTHOG_HOST = "https://eu.i.posthog.com";
21
+ const SERVER_VERSION = "0.5.1";
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.4.1",
3
+ "version": "0.5.1",
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",
@@ -26,7 +26,8 @@
26
26
  "author": "Opedd",
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
- "@modelcontextprotocol/sdk": "^1.0.0"
29
+ "@modelcontextprotocol/sdk": "^1.0.0",
30
+ "posthog-node": "^4.0.0"
30
31
  },
31
32
  "devDependencies": {
32
33
  "@types/node": "^22.19.19",
@@ -36,5 +37,8 @@
36
37
  },
37
38
  "engines": {
38
39
  "node": ">=20"
39
- }
40
+ },
41
+ "files": [
42
+ "dist"
43
+ ]
40
44
  }
@@ -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"