sitezen-mcp 1.0.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/dist/index.js ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SiteZen MCP server entry point.
4
+ *
5
+ * Boots the MCP server over stdio (the transport Claude Desktop and most
6
+ * MCP clients use). All tool definitions live in `tools.ts` — this file
7
+ * just wires the server up and starts listening.
8
+ *
9
+ * Usage (after `npm install` + `npm run build`):
10
+ * sitezen-mcp # via the package bin
11
+ * node dist/index.js # direct
12
+ * npx -y @sitezen/mcp # one-shot (once published)
13
+ *
14
+ * Required env vars (set in Claude Desktop's MCP server config):
15
+ * SITEZEN_SITE_URL e.g. https://yoursite.com
16
+ * SITEZEN_CONNECTION_KEY from WP Admin → SiteZen → Connection
17
+ */
18
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
19
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
20
+ import { registerAllTools } from "./tools.js";
21
+ async function main() {
22
+ const server = new McpServer({
23
+ name: "sitezen-mcp",
24
+ version: "0.1.0",
25
+ });
26
+ registerAllTools(server);
27
+ const transport = new StdioServerTransport();
28
+ await server.connect(transport);
29
+ // Stdio transport stays open until the client disconnects. No further
30
+ // code runs here — Node keeps the process alive on the transport's
31
+ // open file descriptors.
32
+ }
33
+ main().catch((err) => {
34
+ // MCP clients read stdout for the protocol — keep error output on stderr.
35
+ process.stderr.write(`[sitezen-mcp] fatal: ${err instanceof Error ? err.stack || err.message : String(err)}\n`);
36
+ process.exit(1);
37
+ });
@@ -0,0 +1,121 @@
1
+ // License validation for the SiteZen MCP.
2
+ //
3
+ // Today: stubbed locally — any non-empty key returns the FREE plan so the
4
+ // product flow is testable end-to-end while the real dashboard is built.
5
+ // Override later by setting env SITEZEN_LICENSE_API_URL to the production
6
+ // validate endpoint — the MCP will POST {license_key, action} and use the
7
+ // real response instead of the stub.
8
+ //
9
+ // Validated responses are cached on disk (state.json -> license_cache) for
10
+ // 1 hour so we don't hit the network on every conversion. Cache is keyed by
11
+ // SHA-256 of the raw key so changing the key in the config invalidates the
12
+ // cache automatically.
13
+ import * as crypto from "node:crypto";
14
+ import { readState, writeState } from "./state.js";
15
+ const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
16
+ const UPGRADE_URL = "https://sitezen.io/pricing";
17
+ function hashKey(key) {
18
+ return crypto.createHash("sha256").update(key.trim()).digest("hex");
19
+ }
20
+ function cacheIsFresh(cache, key) {
21
+ if (cache.key_hash !== hashKey(key))
22
+ return false;
23
+ const age = Date.now() - new Date(cache.cached_at).getTime();
24
+ return age < CACHE_TTL_MS;
25
+ }
26
+ async function callValidateEndpoint(key) {
27
+ const apiUrl = process.env.SITEZEN_LICENSE_API_URL;
28
+ if (!apiUrl)
29
+ return null;
30
+ try {
31
+ const r = await fetch(apiUrl, {
32
+ method: "POST",
33
+ headers: { "Content-Type": "application/json" },
34
+ body: JSON.stringify({ license_key: key, action: "validate" }),
35
+ });
36
+ if (!r.ok)
37
+ return null;
38
+ const j = await r.json();
39
+ if (!j || j.ok !== true)
40
+ return null;
41
+ return {
42
+ ok: true,
43
+ plan: j.plan,
44
+ sites_allowed: typeof j.sites_allowed === "number" ? j.sites_allowed : 1,
45
+ conversions_remaining: typeof j.conversions_remaining === "number" ? j.conversions_remaining : 0,
46
+ upgrade_url: j.upgrade_url || UPGRADE_URL,
47
+ cached: false,
48
+ };
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ function stubValidate(key) {
55
+ // Stub: any non-empty key → FREE plan. Replace by setting
56
+ // SITEZEN_LICENSE_API_URL in env.
57
+ return {
58
+ ok: true,
59
+ plan: "free",
60
+ sites_allowed: 1,
61
+ conversions_remaining: 3,
62
+ upgrade_url: UPGRADE_URL,
63
+ cached: false,
64
+ };
65
+ }
66
+ /**
67
+ * Validate the user's license key. Returns null if the env var is missing or
68
+ * the key is empty — caller should surface Errors.noLicenseKey() then.
69
+ * Always cached for 1 hour.
70
+ */
71
+ export async function validateLicense(licenseKey) {
72
+ if (!licenseKey || !licenseKey.trim())
73
+ return null;
74
+ const key = licenseKey.trim();
75
+ // Hit cache first
76
+ const state = readState();
77
+ if (state.license_cache && cacheIsFresh(state.license_cache, key)) {
78
+ return {
79
+ ok: true,
80
+ plan: state.license_cache.plan,
81
+ sites_allowed: state.license_cache.sites_allowed,
82
+ conversions_remaining: state.license_cache.conversions_remaining,
83
+ upgrade_url: UPGRADE_URL,
84
+ cached: true,
85
+ };
86
+ }
87
+ // Fresh validation — real endpoint if configured, else stub
88
+ const fresh = (await callValidateEndpoint(key)) ?? stubValidate(key);
89
+ // Persist to cache
90
+ state.license_cache = {
91
+ key_hash: hashKey(key),
92
+ plan: fresh.plan,
93
+ sites_allowed: fresh.sites_allowed,
94
+ conversions_remaining: fresh.conversions_remaining,
95
+ cached_at: new Date().toISOString(),
96
+ };
97
+ writeState(state);
98
+ return fresh;
99
+ }
100
+ /**
101
+ * Decrement the cached conversion count after a successful conversion.
102
+ * Unlimited (-1) is a no-op. Falls through silently if no cache exists
103
+ * (next call to validateLicense rebuilds it).
104
+ */
105
+ export function decrementConversionsRemaining() {
106
+ const state = readState();
107
+ if (!state.license_cache)
108
+ return;
109
+ if (state.license_cache.conversions_remaining < 0)
110
+ return; // unlimited
111
+ state.license_cache.conversions_remaining = Math.max(0, state.license_cache.conversions_remaining - 1);
112
+ writeState(state);
113
+ }
114
+ export function planLabel(plan) {
115
+ switch (plan) {
116
+ case "free": return "Free";
117
+ case "solo": return "Solo";
118
+ case "multi": return "Multi";
119
+ case "agency": return "Agency";
120
+ }
121
+ }