webcake-storefront-mcp 1.0.3 → 1.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/dist/auth/oauth-server.js +236 -0
- package/dist/http.js +289 -10
- package/dist/legal.js +127 -0
- package/dist/web-guide.js +370 -0
- package/package.json +1 -1
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A THIN OAuth 2.1 Authorization Server, embedded in the MCP server itself.
|
|
3
|
+
*
|
|
4
|
+
* Ported from webcake-landing-mcp with one key adaptation: the credential is a
|
|
5
|
+
* PAIR (jwt + wsid) instead of a single `ljwt`. The consent page is the
|
|
6
|
+
* storefront's /mcp-storefront (not /mcp-connect), and the /oauth/callback
|
|
7
|
+
* receives both `token` (jwt) and `wsid` from the SPA. Both are stored against
|
|
8
|
+
* the authorization code and carried through to the access token so the HTTP
|
|
9
|
+
* layer can inject BOTH x-webcake-jwt AND x-webcake-session-id headers.
|
|
10
|
+
*
|
|
11
|
+
* Store: in-memory only (no Postgres dependency in the storefront MCP). For a
|
|
12
|
+
* multi-instance deploy, replace with a shared store behind the same interface.
|
|
13
|
+
*/
|
|
14
|
+
import { randomBytes, createHash } from "node:crypto";
|
|
15
|
+
// ---- TTLs ------------------------------------------------------------------
|
|
16
|
+
const TEN_MIN = 10 * 60 * 1000;
|
|
17
|
+
const ACCESS_TTL = Number(process.env.WEBCAKE_OAUTH_ACCESS_TTL_MS) || 60 * 60 * 1000; // 1h
|
|
18
|
+
const REFRESH_TTL = Number(process.env.WEBCAKE_OAUTH_REFRESH_TTL_MS) || 30 * 24 * 60 * 60 * 1000; // 30d
|
|
19
|
+
const CODE_TTL = TEN_MIN;
|
|
20
|
+
const PENDING_TTL = TEN_MIN;
|
|
21
|
+
function now() {
|
|
22
|
+
return Date.now();
|
|
23
|
+
}
|
|
24
|
+
function token(bytes = 32) {
|
|
25
|
+
return randomBytes(bytes).toString("base64url");
|
|
26
|
+
}
|
|
27
|
+
// ---- In-memory store -------------------------------------------------------
|
|
28
|
+
class MemoryStore {
|
|
29
|
+
clients = new Map();
|
|
30
|
+
pending = new Map();
|
|
31
|
+
codes = new Map();
|
|
32
|
+
access = new Map();
|
|
33
|
+
refresh = new Map();
|
|
34
|
+
sweep() {
|
|
35
|
+
const t = now();
|
|
36
|
+
for (const [k, v] of this.pending)
|
|
37
|
+
if (v.expiresAt < t)
|
|
38
|
+
this.pending.delete(k);
|
|
39
|
+
for (const [k, v] of this.codes)
|
|
40
|
+
if (v.expiresAt < t)
|
|
41
|
+
this.codes.delete(k);
|
|
42
|
+
for (const [k, v] of this.access)
|
|
43
|
+
if (v.expiresAt < t)
|
|
44
|
+
this.access.delete(k);
|
|
45
|
+
for (const [k, v] of this.refresh)
|
|
46
|
+
if (v.expiresAt < t)
|
|
47
|
+
this.refresh.delete(k);
|
|
48
|
+
}
|
|
49
|
+
putClient(c) { this.clients.set(c.client_id, c); }
|
|
50
|
+
getClient(id) { return this.clients.get(id); }
|
|
51
|
+
putPending(state, p) { this.sweep(); this.pending.set(state, p); }
|
|
52
|
+
takePending(state) {
|
|
53
|
+
this.sweep();
|
|
54
|
+
const p = this.pending.get(state);
|
|
55
|
+
this.pending.delete(state);
|
|
56
|
+
return p && p.expiresAt >= now() ? p : undefined;
|
|
57
|
+
}
|
|
58
|
+
putCode(code, c) { this.codes.set(code, c); }
|
|
59
|
+
takeCode(code) {
|
|
60
|
+
this.sweep();
|
|
61
|
+
const c = this.codes.get(code);
|
|
62
|
+
this.codes.delete(code);
|
|
63
|
+
return c && c.expiresAt >= now() ? c : undefined;
|
|
64
|
+
}
|
|
65
|
+
putAccess(t, a) { this.access.set(t, a); }
|
|
66
|
+
getAccess(t) {
|
|
67
|
+
const a = this.access.get(t);
|
|
68
|
+
if (a && a.expiresAt < now()) {
|
|
69
|
+
this.access.delete(t);
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
return a;
|
|
73
|
+
}
|
|
74
|
+
putRefresh(t, r) { this.refresh.set(t, r); }
|
|
75
|
+
takeRefresh(t) {
|
|
76
|
+
this.sweep();
|
|
77
|
+
const r = this.refresh.get(t);
|
|
78
|
+
this.refresh.delete(t);
|
|
79
|
+
return r && r.expiresAt >= now() ? r : undefined;
|
|
80
|
+
}
|
|
81
|
+
revoke(t) { this.access.delete(t); this.refresh.delete(t); }
|
|
82
|
+
}
|
|
83
|
+
const store = new MemoryStore();
|
|
84
|
+
// ---- PKCE ------------------------------------------------------------------
|
|
85
|
+
export function s256(verifier) {
|
|
86
|
+
return createHash("sha256").update(verifier).digest("base64url");
|
|
87
|
+
}
|
|
88
|
+
export function verifyPkce(verifier, challenge) {
|
|
89
|
+
if (!verifier || !challenge)
|
|
90
|
+
return false;
|
|
91
|
+
const a = s256(verifier);
|
|
92
|
+
if (a.length !== challenge.length)
|
|
93
|
+
return false;
|
|
94
|
+
let diff = 0;
|
|
95
|
+
for (let i = 0; i < a.length; i++)
|
|
96
|
+
diff |= a.charCodeAt(i) ^ challenge.charCodeAt(i);
|
|
97
|
+
return diff === 0;
|
|
98
|
+
}
|
|
99
|
+
export function registerClient(body) {
|
|
100
|
+
const uris = Array.isArray(body?.redirect_uris)
|
|
101
|
+
? body.redirect_uris.filter((u) => typeof u === "string" && /^https?:\/\//i.test(u))
|
|
102
|
+
: [];
|
|
103
|
+
if (uris.length === 0) {
|
|
104
|
+
return { ok: false, error: "invalid_redirect_uri", error_description: "redirect_uris must contain at least one absolute http(s) URI." };
|
|
105
|
+
}
|
|
106
|
+
const client = {
|
|
107
|
+
client_id: token(16),
|
|
108
|
+
client_name: typeof body?.client_name === "string" ? body.client_name : undefined,
|
|
109
|
+
redirect_uris: uris,
|
|
110
|
+
created_at: now(),
|
|
111
|
+
};
|
|
112
|
+
store.putClient(client);
|
|
113
|
+
return { ok: true, client };
|
|
114
|
+
}
|
|
115
|
+
export function getClient(clientId) {
|
|
116
|
+
if (!clientId)
|
|
117
|
+
return undefined;
|
|
118
|
+
return store.getClient(clientId);
|
|
119
|
+
}
|
|
120
|
+
export function startAuthorize(p) {
|
|
121
|
+
const client = p.client_id ? store.getClient(p.client_id) : undefined;
|
|
122
|
+
if (!client) {
|
|
123
|
+
return { ok: false, error: "invalid_client", error_description: "Unknown client_id. Register first via /register.", redirectable: false };
|
|
124
|
+
}
|
|
125
|
+
if (!p.redirect_uri || !client.redirect_uris.includes(p.redirect_uri)) {
|
|
126
|
+
return { ok: false, error: "invalid_request", error_description: "redirect_uri does not match a registered URI.", redirectable: false };
|
|
127
|
+
}
|
|
128
|
+
if (p.response_type !== "code") {
|
|
129
|
+
return { ok: false, error: "unsupported_response_type", error_description: "Only response_type=code is supported.", redirectable: true };
|
|
130
|
+
}
|
|
131
|
+
if (!p.code_challenge || (p.code_challenge_method ?? "").toUpperCase() !== "S256") {
|
|
132
|
+
return { ok: false, error: "invalid_request", error_description: "PKCE with code_challenge_method=S256 is required.", redirectable: true };
|
|
133
|
+
}
|
|
134
|
+
const internalState = token(24);
|
|
135
|
+
store.putPending(internalState, {
|
|
136
|
+
client_id: client.client_id,
|
|
137
|
+
redirect_uri: p.redirect_uri,
|
|
138
|
+
code_challenge: p.code_challenge,
|
|
139
|
+
state: p.state ?? undefined,
|
|
140
|
+
scope: p.scope ?? undefined,
|
|
141
|
+
expiresAt: now() + PENDING_TTL,
|
|
142
|
+
});
|
|
143
|
+
return { ok: true, internalState };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The consent page (/mcp-storefront) bounced back with the user's jwt + wsid and our
|
|
147
|
+
* internalState. Mint a one-time authorization code bound to that credential pair.
|
|
148
|
+
*/
|
|
149
|
+
export function completeAuthorize(internalState, jwt, wsid) {
|
|
150
|
+
const p = internalState ? store.takePending(internalState) : undefined;
|
|
151
|
+
if (!p) {
|
|
152
|
+
return { ok: false, error: "invalid_request", error_description: "Authorization session expired or unknown — restart the connection." };
|
|
153
|
+
}
|
|
154
|
+
if (!jwt) {
|
|
155
|
+
return { ok: false, error: "access_denied", error_description: "No WebCake token returned from login." };
|
|
156
|
+
}
|
|
157
|
+
const code = token(32);
|
|
158
|
+
store.putCode(code, {
|
|
159
|
+
client_id: p.client_id,
|
|
160
|
+
redirect_uri: p.redirect_uri,
|
|
161
|
+
code_challenge: p.code_challenge,
|
|
162
|
+
scope: p.scope,
|
|
163
|
+
cred: { jwt, wsid: wsid ?? "" },
|
|
164
|
+
expiresAt: now() + CODE_TTL,
|
|
165
|
+
});
|
|
166
|
+
return { ok: true, redirectUri: p.redirect_uri, code, state: p.state };
|
|
167
|
+
}
|
|
168
|
+
function issueTokens(cred, client_id, scope) {
|
|
169
|
+
const access = token(32);
|
|
170
|
+
const refresh = token(32);
|
|
171
|
+
store.putAccess(access, { cred, scope, expiresAt: now() + ACCESS_TTL });
|
|
172
|
+
store.putRefresh(refresh, { cred, client_id, scope, expiresAt: now() + REFRESH_TTL });
|
|
173
|
+
return { access_token: access, token_type: "Bearer", expires_in: Math.floor(ACCESS_TTL / 1000), refresh_token: refresh, scope };
|
|
174
|
+
}
|
|
175
|
+
export function exchangeToken(p) {
|
|
176
|
+
if (p.grant_type === "authorization_code") {
|
|
177
|
+
const c = p.code ? store.takeCode(p.code) : undefined;
|
|
178
|
+
if (!c) {
|
|
179
|
+
return { ok: false, status: 400, error: "invalid_grant", error_description: "Unknown or expired authorization code." };
|
|
180
|
+
}
|
|
181
|
+
if (c.client_id !== p.client_id) {
|
|
182
|
+
return { ok: false, status: 400, error: "invalid_grant", error_description: "client_id does not match the authorization code." };
|
|
183
|
+
}
|
|
184
|
+
if (c.redirect_uri !== p.redirect_uri) {
|
|
185
|
+
return { ok: false, status: 400, error: "invalid_grant", error_description: "redirect_uri does not match the authorization request." };
|
|
186
|
+
}
|
|
187
|
+
if (!p.code_verifier || !verifyPkce(p.code_verifier, c.code_challenge)) {
|
|
188
|
+
return { ok: false, status: 400, error: "invalid_grant", error_description: "PKCE verification failed." };
|
|
189
|
+
}
|
|
190
|
+
return { ok: true, body: issueTokens(c.cred, c.client_id, c.scope) };
|
|
191
|
+
}
|
|
192
|
+
if (p.grant_type === "refresh_token") {
|
|
193
|
+
const r = p.refresh_token ? store.takeRefresh(p.refresh_token) : undefined;
|
|
194
|
+
if (!r) {
|
|
195
|
+
return { ok: false, status: 400, error: "invalid_grant", error_description: "Unknown or expired refresh token." };
|
|
196
|
+
}
|
|
197
|
+
return { ok: true, body: issueTokens(r.cred, r.client_id, r.scope) };
|
|
198
|
+
}
|
|
199
|
+
return { ok: false, status: 400, error: "unsupported_grant_type", error_description: "grant_type must be authorization_code or refresh_token." };
|
|
200
|
+
}
|
|
201
|
+
// ---- Resource-server side: resolve a Bearer access token to the cred pair --
|
|
202
|
+
/** Returns the { jwt, wsid } for a valid, unexpired access token, else undefined. */
|
|
203
|
+
export function resolveAccessToken(accessToken) {
|
|
204
|
+
if (!accessToken)
|
|
205
|
+
return undefined;
|
|
206
|
+
return store.getAccess(accessToken)?.cred;
|
|
207
|
+
}
|
|
208
|
+
/** Revoke an access or refresh token (best-effort; for /revoke). */
|
|
209
|
+
export function revokeToken(t) {
|
|
210
|
+
if (!t)
|
|
211
|
+
return;
|
|
212
|
+
store.revoke(t);
|
|
213
|
+
}
|
|
214
|
+
// ---- Metadata documents (RFC 8414 / RFC 9728) ------------------------------
|
|
215
|
+
export function authServerMetadata(issuer) {
|
|
216
|
+
return {
|
|
217
|
+
issuer,
|
|
218
|
+
authorization_endpoint: `${issuer}/authorize`,
|
|
219
|
+
token_endpoint: `${issuer}/token`,
|
|
220
|
+
registration_endpoint: `${issuer}/register`,
|
|
221
|
+
revocation_endpoint: `${issuer}/revoke`,
|
|
222
|
+
response_types_supported: ["code"],
|
|
223
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
224
|
+
code_challenge_methods_supported: ["S256"],
|
|
225
|
+
token_endpoint_auth_methods_supported: ["none"],
|
|
226
|
+
scopes_supported: ["storefront:read", "storefront:write"],
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
export function protectedResourceMetadata(resource, issuer) {
|
|
230
|
+
return {
|
|
231
|
+
resource,
|
|
232
|
+
authorization_servers: [issuer],
|
|
233
|
+
scopes_supported: ["storefront:read", "storefront:write"],
|
|
234
|
+
bearer_methods_supported: ["header"],
|
|
235
|
+
};
|
|
236
|
+
}
|
package/dist/http.js
CHANGED
|
@@ -1,13 +1,34 @@
|
|
|
1
1
|
// Remote MCP over Streamable-HTTP. Each client session carries its own credentials,
|
|
2
2
|
// supplied per-request via headers (x-webcake-jwt / x-webcake-site-id / x-webcake-api-url)
|
|
3
3
|
// or query params (?jwt=&site_id=&api_url=) for clients that can't set custom headers.
|
|
4
|
+
//
|
|
5
|
+
// Also serves: a marketing landing page at /, /privacy, /terms, /favicon.ico,
|
|
6
|
+
// /favicon.svg, /health, and a full OAuth 2.1 flow so the claude.ai connector can
|
|
7
|
+
// authenticate. See src/auth/oauth-server.ts for the OAuth AS implementation.
|
|
4
8
|
import { createServer as createHttpServer } from "node:http";
|
|
5
9
|
import { randomUUID } from "node:crypto";
|
|
6
10
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
7
11
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
8
12
|
import { createServer } from "./server.js";
|
|
9
|
-
import { makeApi } from "./config.js";
|
|
13
|
+
import { makeApi, resolveEnv, ENVIRONMENTS, DEFAULT_ENV } from "./config.js";
|
|
14
|
+
import { landingHtml, faviconSvg } from "./web-guide.js";
|
|
15
|
+
import { privacyHtml, termsHtml } from "./legal.js";
|
|
16
|
+
import { registerClient, startAuthorize, completeAuthorize, exchangeToken, resolveAccessToken, revokeToken, authServerMetadata, protectedResourceMetadata, } from "./auth/oauth-server.js";
|
|
10
17
|
const MCP_PATH = "/mcp";
|
|
18
|
+
// OAuth 2.1 endpoints.
|
|
19
|
+
const WELL_KNOWN_PR = "/.well-known/oauth-protected-resource";
|
|
20
|
+
const WELL_KNOWN_AS = "/.well-known/oauth-authorization-server";
|
|
21
|
+
const OAUTH_REGISTER = "/register";
|
|
22
|
+
const OAUTH_AUTHORIZE = "/authorize";
|
|
23
|
+
const OAUTH_CALLBACK = "/oauth/callback";
|
|
24
|
+
const OAUTH_TOKEN = "/token";
|
|
25
|
+
const OAUTH_REVOKE = "/revoke";
|
|
26
|
+
// OAuth enforcement is ON by default: a request with NO credential gets a
|
|
27
|
+
// 401 + WWW-Authenticate so Claude/ChatGPT kick off the OAuth flow.
|
|
28
|
+
// Opt out with WEBCAKE_OAUTH=0 (or false/no/off).
|
|
29
|
+
const OAUTH_ENFORCED = !/^(0|false|no|off)$/i.test(process.env.WEBCAKE_OAUTH ?? "");
|
|
30
|
+
// Social/search crawlers fetch the root with Accept: */* but should still get HTML.
|
|
31
|
+
const BOT_UA = /facebookexternalhit|facebot|twitterbot|linkedinbot|slackbot|slack-imgproxy|telegrambot|whatsapp|discordbot|pinterest|redditbot|googlebot|bingbot|applebot|yandexbot|baiduspider|embedly|quora link preview|outbrain|vkshare|w3c_validator|skypeuripreview|zalo/i;
|
|
11
32
|
const QUERY_TO_HEADER = {
|
|
12
33
|
jwt: "x-webcake-jwt",
|
|
13
34
|
token: "x-webcake-jwt",
|
|
@@ -28,8 +49,10 @@ function applyQueryAuth(req) {
|
|
|
28
49
|
const params = new URLSearchParams((req.url ?? "").slice(q + 1));
|
|
29
50
|
for (const [param, head] of Object.entries(QUERY_TO_HEADER)) {
|
|
30
51
|
const value = params.get(param);
|
|
31
|
-
if (value && req.headers[head] == null)
|
|
52
|
+
if (value && req.headers[head] == null) {
|
|
32
53
|
req.headers[head] = value;
|
|
54
|
+
req.rawHeaders.push(head, value);
|
|
55
|
+
}
|
|
33
56
|
}
|
|
34
57
|
}
|
|
35
58
|
function apiFromRequest(req) {
|
|
@@ -59,23 +82,278 @@ function readBody(req) {
|
|
|
59
82
|
req.on("error", reject);
|
|
60
83
|
});
|
|
61
84
|
}
|
|
62
|
-
function
|
|
85
|
+
async function readRawBody(req) {
|
|
86
|
+
const chunks = [];
|
|
87
|
+
for await (const c of req)
|
|
88
|
+
chunks.push(c);
|
|
89
|
+
return Buffer.concat(chunks).toString("utf8").trim();
|
|
90
|
+
}
|
|
91
|
+
function parseBodyParams(raw, contentType) {
|
|
92
|
+
if (!raw)
|
|
93
|
+
return {};
|
|
94
|
+
if (contentType.includes("application/json")) {
|
|
95
|
+
try {
|
|
96
|
+
const o = JSON.parse(raw);
|
|
97
|
+
return o && typeof o === "object" ? o : {};
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const out = {};
|
|
104
|
+
for (const [k, v] of new URLSearchParams(raw))
|
|
105
|
+
out[k] = v;
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
function sendJson(res, status, body) {
|
|
63
109
|
res.writeHead(status, { "content-type": "application/json" });
|
|
64
|
-
res.end(JSON.stringify(
|
|
110
|
+
res.end(JSON.stringify(body));
|
|
111
|
+
}
|
|
112
|
+
function rpcError(res, status, message) {
|
|
113
|
+
sendJson(res, status, { jsonrpc: "2.0", error: { code: -32000, message }, id: null });
|
|
114
|
+
}
|
|
115
|
+
function oauthError(res, status, error, description) {
|
|
116
|
+
res.writeHead(status, { "content-type": "application/json", "cache-control": "no-store" });
|
|
117
|
+
res.end(JSON.stringify({ error, error_description: description }));
|
|
118
|
+
}
|
|
119
|
+
function htmlError(res, status, message) {
|
|
120
|
+
res.writeHead(status, { "content-type": "text/html; charset=utf-8" });
|
|
121
|
+
res.end(`<!doctype html><meta charset="utf-8"><body style="font-family:system-ui;padding:40px;max-width:520px;margin:auto"><h2>WebCake Storefront MCP</h2><p>${message}</p></body>`);
|
|
122
|
+
}
|
|
123
|
+
/** The public origin of this server, honouring reverse-proxy headers. */
|
|
124
|
+
function publicBase(req) {
|
|
125
|
+
const fwdHost = req.headers["x-forwarded-host"];
|
|
126
|
+
const host = (Array.isArray(fwdHost) ? fwdHost[0] : fwdHost) || req.headers.host || "localhost";
|
|
127
|
+
const fwdProto = req.headers["x-forwarded-proto"];
|
|
128
|
+
const isLocal = /^(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/i.test(host);
|
|
129
|
+
const proto = (Array.isArray(fwdProto) ? fwdProto[0] : fwdProto)?.split(",")[0] || (isLocal ? "http" : "https");
|
|
130
|
+
return `${proto}://${host}`;
|
|
131
|
+
}
|
|
132
|
+
/** The storefront consent page URL — /mcp-storefront on the builder app. */
|
|
133
|
+
function storefrontConsentUrl() {
|
|
134
|
+
const preset = resolveEnv(process.env.WEBCAKE_ENV) ?? ENVIRONMENTS[DEFAULT_ENV];
|
|
135
|
+
const appUrl = (process.env.WEBCAKE_APP_URL || preset.appUrl).replace(/\/$/, "");
|
|
136
|
+
return `${appUrl}/mcp-storefront`;
|
|
137
|
+
}
|
|
138
|
+
/** Extract the Bearer token from the Authorization header, if any. */
|
|
139
|
+
function bearerFrom(req) {
|
|
140
|
+
const auth = req.headers["authorization"];
|
|
141
|
+
const v = Array.isArray(auth) ? auth[0] : auth;
|
|
142
|
+
if (!v || !/^Bearer\s+/i.test(v))
|
|
143
|
+
return undefined;
|
|
144
|
+
return v.replace(/^Bearer\s+/i, "").trim() || undefined;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Handle every OAuth 2.1 endpoint. Returns true when the request was handled.
|
|
148
|
+
*
|
|
149
|
+
* KEY ADAPTATION vs. landing-mcp: the credential is a PAIR { jwt, wsid }.
|
|
150
|
+
* The /oauth/callback receives both `token` (jwt) and `wsid` from the consent
|
|
151
|
+
* page redirect_uri. Both are stored and later injected as x-webcake-jwt AND
|
|
152
|
+
* x-webcake-session-id headers onto /mcp requests.
|
|
153
|
+
*/
|
|
154
|
+
async function handleOAuth(req, res, path) {
|
|
155
|
+
const issuer = publicBase(req);
|
|
156
|
+
// ---- Metadata ----
|
|
157
|
+
if (req.method === "GET" && path === WELL_KNOWN_PR) {
|
|
158
|
+
res.writeHead(200, { "content-type": "application/json", "access-control-allow-origin": "*" });
|
|
159
|
+
res.end(JSON.stringify(protectedResourceMetadata(`${issuer}${MCP_PATH}`, issuer)));
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
if (req.method === "GET" && path === WELL_KNOWN_AS) {
|
|
163
|
+
res.writeHead(200, { "content-type": "application/json", "access-control-allow-origin": "*" });
|
|
164
|
+
res.end(JSON.stringify(authServerMetadata(issuer)));
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
// ---- Dynamic Client Registration ----
|
|
168
|
+
if (path === OAUTH_REGISTER) {
|
|
169
|
+
if (req.method === "OPTIONS") {
|
|
170
|
+
res.writeHead(204, { "access-control-allow-origin": "*", "access-control-allow-headers": "*", "access-control-allow-methods": "POST,OPTIONS" });
|
|
171
|
+
res.end();
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
if (req.method !== "POST") {
|
|
175
|
+
oauthError(res, 405, "invalid_request", "Use POST.");
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
const raw = await readRawBody(req);
|
|
179
|
+
const body = parseBodyParams(raw, String(req.headers["content-type"] ?? ""));
|
|
180
|
+
const result = registerClient(body);
|
|
181
|
+
if (!result.ok) {
|
|
182
|
+
oauthError(res, 400, result.error, result.error_description);
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
res.writeHead(201, { "content-type": "application/json", "access-control-allow-origin": "*", "cache-control": "no-store" });
|
|
186
|
+
res.end(JSON.stringify({
|
|
187
|
+
client_id: result.client.client_id,
|
|
188
|
+
client_id_issued_at: Math.floor(result.client.created_at / 1000),
|
|
189
|
+
redirect_uris: result.client.redirect_uris,
|
|
190
|
+
token_endpoint_auth_method: "none",
|
|
191
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
192
|
+
response_types: ["code"],
|
|
193
|
+
}));
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
// ---- Authorize: validate + redirect to storefront consent page ----
|
|
197
|
+
if (req.method === "GET" && path === OAUTH_AUTHORIZE) {
|
|
198
|
+
const sp = new URL(req.url ?? "/", "http://x").searchParams;
|
|
199
|
+
const result = startAuthorize({
|
|
200
|
+
client_id: sp.get("client_id"),
|
|
201
|
+
redirect_uri: sp.get("redirect_uri"),
|
|
202
|
+
response_type: sp.get("response_type"),
|
|
203
|
+
code_challenge: sp.get("code_challenge"),
|
|
204
|
+
code_challenge_method: sp.get("code_challenge_method"),
|
|
205
|
+
state: sp.get("state"),
|
|
206
|
+
scope: sp.get("scope"),
|
|
207
|
+
});
|
|
208
|
+
if (!result.ok) {
|
|
209
|
+
if (result.redirectable) {
|
|
210
|
+
const r = new URL(sp.get("redirect_uri"));
|
|
211
|
+
r.searchParams.set("error", result.error);
|
|
212
|
+
r.searchParams.set("error_description", result.error_description);
|
|
213
|
+
const st = sp.get("state");
|
|
214
|
+
if (st)
|
|
215
|
+
r.searchParams.set("state", st);
|
|
216
|
+
res.writeHead(302, { location: r.toString() });
|
|
217
|
+
res.end();
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
htmlError(res, 400, result.error_description);
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
// Build the consent URL: /mcp-storefront?redirect_uri=<callback>&state=<internalState>
|
|
224
|
+
// The SPA will redirect back to callback with ?token=<jwt>&wsid=<wsid>&state=<internalState>
|
|
225
|
+
const callback = `${issuer}${OAUTH_CALLBACK}`;
|
|
226
|
+
const consentBase = storefrontConsentUrl();
|
|
227
|
+
const loginUrl = `${consentBase}?redirect_uri=${encodeURIComponent(callback)}&state=${encodeURIComponent(result.internalState)}`;
|
|
228
|
+
res.writeHead(302, { location: loginUrl });
|
|
229
|
+
res.end();
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
// ---- Callback: SPA sent back token (jwt) + wsid + state ----
|
|
233
|
+
if (req.method === "GET" && path === OAUTH_CALLBACK) {
|
|
234
|
+
const sp = new URL(req.url ?? "/", "http://x").searchParams;
|
|
235
|
+
// Accept both 'token' and 'jwt' from the SPA (login.ts uses both aliases).
|
|
236
|
+
const jwt = sp.get("token") || sp.get("jwt");
|
|
237
|
+
const wsid = sp.get("wsid") || sp.get("session_id") || "";
|
|
238
|
+
const done = completeAuthorize(sp.get("state"), jwt, wsid);
|
|
239
|
+
if (!done.ok) {
|
|
240
|
+
htmlError(res, 400, done.error_description);
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
const r = new URL(done.redirectUri);
|
|
244
|
+
r.searchParams.set("code", done.code);
|
|
245
|
+
if (done.state)
|
|
246
|
+
r.searchParams.set("state", done.state);
|
|
247
|
+
res.writeHead(302, { location: r.toString() });
|
|
248
|
+
res.end();
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
// ---- Token ----
|
|
252
|
+
if (path === OAUTH_TOKEN) {
|
|
253
|
+
if (req.method === "OPTIONS") {
|
|
254
|
+
res.writeHead(204, { "access-control-allow-origin": "*", "access-control-allow-headers": "*", "access-control-allow-methods": "POST,OPTIONS" });
|
|
255
|
+
res.end();
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
if (req.method !== "POST") {
|
|
259
|
+
oauthError(res, 405, "invalid_request", "Use POST.");
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
const raw = await readRawBody(req);
|
|
263
|
+
const body = parseBodyParams(raw, String(req.headers["content-type"] ?? ""));
|
|
264
|
+
const result = exchangeToken(body);
|
|
265
|
+
if (!result.ok) {
|
|
266
|
+
oauthError(res, result.status, result.error, result.error_description);
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
res.writeHead(200, { "content-type": "application/json", "access-control-allow-origin": "*", "cache-control": "no-store" });
|
|
270
|
+
res.end(JSON.stringify(result.body));
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
// ---- Revoke ----
|
|
274
|
+
if (path === OAUTH_REVOKE) {
|
|
275
|
+
if (req.method !== "POST") {
|
|
276
|
+
oauthError(res, 405, "invalid_request", "Use POST.");
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
const raw = await readRawBody(req);
|
|
280
|
+
const body = parseBodyParams(raw, String(req.headers["content-type"] ?? ""));
|
|
281
|
+
revokeToken(body.token);
|
|
282
|
+
res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
|
|
283
|
+
res.end("{}");
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
65
287
|
}
|
|
66
288
|
export async function startHttpServer(port) {
|
|
67
289
|
const transports = new Map();
|
|
68
290
|
const httpServer = createHttpServer(async (req, res) => {
|
|
69
291
|
const path = (req.url ?? "").split("?")[0];
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
res.
|
|
292
|
+
// ---- Favicon / brand icon ----
|
|
293
|
+
if (req.method === "GET" && (path === "/favicon.ico" || path === "/favicon.svg")) {
|
|
294
|
+
res.writeHead(200, { "content-type": "image/svg+xml", "cache-control": "public, max-age=86400" });
|
|
295
|
+
res.end(faviconSvg());
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
// ---- Legal pages ----
|
|
299
|
+
if (req.method === "GET" && (path === "/privacy" || path === "/privacy-policy")) {
|
|
300
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "public, max-age=3600" });
|
|
301
|
+
res.end(privacyHtml());
|
|
73
302
|
return;
|
|
74
303
|
}
|
|
304
|
+
if (req.method === "GET" && (path === "/terms" || path === "/tos")) {
|
|
305
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "public, max-age=3600" });
|
|
306
|
+
res.end(termsHtml());
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
// ---- Health + landing page ----
|
|
310
|
+
if (req.method === "GET" && (path === "/" || path === "/health")) {
|
|
311
|
+
if (path === "/") {
|
|
312
|
+
const accept = String(req.headers["accept"] ?? "");
|
|
313
|
+
const ua = String(req.headers["user-agent"] ?? "");
|
|
314
|
+
if (accept.includes("text/html") || BOT_UA.test(ua)) {
|
|
315
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
316
|
+
res.end(landingHtml(publicBase(req)));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
sendJson(res, 200, { ok: true, server: "webcake-storefront", transport: "streamable-http", endpoint: MCP_PATH });
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
// ---- OAuth 2.1 endpoints (always served) ----
|
|
324
|
+
if (await handleOAuth(req, res, path))
|
|
325
|
+
return;
|
|
326
|
+
// ---- 404 for anything other than /mcp ----
|
|
75
327
|
if (path !== MCP_PATH) {
|
|
76
|
-
|
|
328
|
+
rpcError(res, 404, `Not found. Send MCP requests to ${MCP_PATH}.`);
|
|
329
|
+
return;
|
|
77
330
|
}
|
|
331
|
+
// ---- /mcp handling ----
|
|
332
|
+
// Accept credentials via query params for clients that can't set headers.
|
|
78
333
|
applyQueryAuth(req);
|
|
334
|
+
// Resolve an OAuth Bearer access token to { jwt, wsid } and inject both headers
|
|
335
|
+
// so apiFromRequest picks them up — existing header/query paths remain untouched.
|
|
336
|
+
const bearer = bearerFrom(req);
|
|
337
|
+
const oauthCred = resolveAccessToken(bearer);
|
|
338
|
+
if (oauthCred) {
|
|
339
|
+
if (req.headers["x-webcake-jwt"] == null) {
|
|
340
|
+
req.headers["x-webcake-jwt"] = oauthCred.jwt;
|
|
341
|
+
req.rawHeaders.push("x-webcake-jwt", oauthCred.jwt);
|
|
342
|
+
}
|
|
343
|
+
if (oauthCred.wsid && req.headers["x-webcake-session-id"] == null) {
|
|
344
|
+
req.headers["x-webcake-session-id"] = oauthCred.wsid;
|
|
345
|
+
req.rawHeaders.push("x-webcake-session-id", oauthCred.wsid);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// Enforce OAuth when enabled: a request with no recognised credential gets 401.
|
|
349
|
+
if (OAUTH_ENFORCED && !oauthCred && req.headers["x-webcake-jwt"] == null) {
|
|
350
|
+
res.writeHead(401, {
|
|
351
|
+
"www-authenticate": `Bearer resource_metadata="${publicBase(req)}${WELL_KNOWN_PR}"`,
|
|
352
|
+
"content-type": "application/json",
|
|
353
|
+
});
|
|
354
|
+
res.end(JSON.stringify({ error: "invalid_token", error_description: "Authentication required — connect via OAuth." }));
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
79
357
|
const sidHeader = header(req, "mcp-session-id");
|
|
80
358
|
try {
|
|
81
359
|
// Reuse an existing session.
|
|
@@ -105,9 +383,10 @@ export async function startHttpServer(port) {
|
|
|
105
383
|
await transport.handleRequest(req, res, body);
|
|
106
384
|
return;
|
|
107
385
|
}
|
|
108
|
-
|
|
386
|
+
rpcError(res, 400, "Bad Request: send an initialize request first (no valid mcp-session-id).");
|
|
387
|
+
return;
|
|
109
388
|
}
|
|
110
|
-
|
|
389
|
+
rpcError(res, 400, "Bad Request: missing or unknown mcp-session-id.");
|
|
111
390
|
}
|
|
112
391
|
catch (e) {
|
|
113
392
|
const msg = e instanceof Error ? e.message : String(e);
|
package/dist/legal.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Privacy Policy + Terms of Service pages for the WebCake Storefront MCP connector.
|
|
3
|
+
*
|
|
4
|
+
* Hosted at /privacy (also /privacy-policy) and /terms (also /tos) so the
|
|
5
|
+
* Claude Connectors Directory submission can point at stable, self-hosted URLs.
|
|
6
|
+
* Plain self-contained HTML — no external deps, served by http.ts.
|
|
7
|
+
*/
|
|
8
|
+
const CONTACT_EMAIL = process.env.WEBCAKE_SUPPORT_EMAIL || "vuluu040320@gmail.com";
|
|
9
|
+
const LAST_UPDATED = "2026-06-23";
|
|
10
|
+
function page(title, bodyHtml) {
|
|
11
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
12
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"><title>${title} — WebCake Storefront MCP</title>
|
|
13
|
+
<style>
|
|
14
|
+
:root{color-scheme:light dark}
|
|
15
|
+
body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;line-height:1.65;color:#1e293b;background:#f8fafc}
|
|
16
|
+
@media(prefers-color-scheme:dark){body{color:#e2e8f0;background:#0f172a}}
|
|
17
|
+
main{max-width:760px;margin:0 auto;padding:48px 24px 80px}
|
|
18
|
+
h1{font-size:1.9rem;margin:0 0 4px}
|
|
19
|
+
h2{font-size:1.2rem;margin:32px 0 8px}
|
|
20
|
+
.meta{color:#64748b;font-size:.9rem;margin-bottom:28px}
|
|
21
|
+
a{color:#108B67}
|
|
22
|
+
code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:rgba(127,127,127,.15);padding:1px 5px;border-radius:4px}
|
|
23
|
+
ul{padding-left:22px}
|
|
24
|
+
footer{margin-top:48px;padding-top:20px;border-top:1px solid rgba(127,127,127,.25);color:#64748b;font-size:.85rem}
|
|
25
|
+
</style></head>
|
|
26
|
+
<body><main>${bodyHtml}
|
|
27
|
+
<footer>WebCake Storefront MCP · <a href="https://webcake.io">webcake.io</a> · <a href="https://github.com/vuluu2k/webcake-storefront-mcp">source</a> · Contact: <a href="mailto:${CONTACT_EMAIL}">${CONTACT_EMAIL}</a></footer>
|
|
28
|
+
</main></body></html>`;
|
|
29
|
+
}
|
|
30
|
+
export function privacyHtml() {
|
|
31
|
+
return page("Privacy Policy", `<h1>Privacy Policy</h1>
|
|
32
|
+
<div class="meta">Last updated: ${LAST_UPDATED}</div>
|
|
33
|
+
<p>WebCake Storefront MCP ("the connector") is a Model Context Protocol server that lets an AI assistant
|
|
34
|
+
build and manage storefront pages, products, articles, and orders in your
|
|
35
|
+
<a href="https://webcake.io">WebCake / StoreCake</a> account. This policy explains what data the connector
|
|
36
|
+
handles, why, who receives it, and how long it is kept.</p>
|
|
37
|
+
|
|
38
|
+
<h2>Categories of personal data we access</h2>
|
|
39
|
+
<ul>
|
|
40
|
+
<li><strong>Your WebCake identity, JWT, and session ID.</strong> When you connect via OAuth, you log in to
|
|
41
|
+
WebCake through the browser and the connector receives your bearer JWT and workspace session ID (<code>wsid</code>).
|
|
42
|
+
These are used <em>solely</em> to call the WebCake / StoreCake backend API on your behalf. They are never
|
|
43
|
+
shared with the AI assistant or any third party.</li>
|
|
44
|
+
<li><strong>Storefront content you ask the assistant to create or edit.</strong> Page source, product details,
|
|
45
|
+
article text, customer look-ups, and order data flow through the connector to your WebCake account.
|
|
46
|
+
<em>Purpose:</em> to carry out the actions you request.</li>
|
|
47
|
+
<li><strong>Images.</strong> When you ask the assistant to resize or process images, they are handled transiently
|
|
48
|
+
in memory and forwarded to the WebCake CDN. No image is retained on the connector after the request completes.</li>
|
|
49
|
+
</ul>
|
|
50
|
+
|
|
51
|
+
<h2>What we store and for how long</h2>
|
|
52
|
+
<ul>
|
|
53
|
+
<li><strong>OAuth tokens (in-process memory only).</strong> The connector holds your JWT and session ID in an
|
|
54
|
+
in-memory token store <strong>only for the lifetime of the server process</strong>. There is no database;
|
|
55
|
+
tokens are never written to disk by the connector. Access tokens expire automatically after ~1 hour, refresh
|
|
56
|
+
tokens after ~30 days. A server restart clears all tokens.</li>
|
|
57
|
+
<li><strong>Local CLI config (stdio mode).</strong> When you run <code>npx webcake-storefront-mcp login</code>,
|
|
58
|
+
your token and session ID are saved to a local SQLite file on <em>your own machine</em> (at
|
|
59
|
+
<code>~/.webcake-storefront-mcp.db</code> or similar). This file stays on your device and is not transmitted
|
|
60
|
+
anywhere by the connector.</li>
|
|
61
|
+
<li>The connector does <strong>not</strong> run an analytics database, does <strong>not</strong> sell or share
|
|
62
|
+
data, and does <strong>not</strong> perform tracking or behavioral profiling.</li>
|
|
63
|
+
</ul>
|
|
64
|
+
|
|
65
|
+
<h2>Categories of recipients</h2>
|
|
66
|
+
<p>Your data is shared only with the services required to fulfil your request:</p>
|
|
67
|
+
<ul>
|
|
68
|
+
<li><strong>WebCake / StoreCake API</strong> (<code>api.storefront.webcake.io</code>) — stores and serves
|
|
69
|
+
your storefront pages, products, and orders; governed by WebCake's own terms.</li>
|
|
70
|
+
<li><strong>WebCake CDN</strong> — hosts images and published page assets that you explicitly create or upload.</li>
|
|
71
|
+
</ul>
|
|
72
|
+
|
|
73
|
+
<h2>Data we do NOT collect</h2>
|
|
74
|
+
<p>The connector never asks for or stores payment-card data, passwords, health data, government identifiers,
|
|
75
|
+
or MFA/OTP codes. It operates only on the storefront content you explicitly instruct the assistant to
|
|
76
|
+
create — it does <strong>not</strong> read, reconstruct, or infer your conversation history.</p>
|
|
77
|
+
|
|
78
|
+
<h2>Data retention & deletion</h2>
|
|
79
|
+
<p>OAuth tokens expire automatically or are cleared on server restart. You can revoke access at any time by
|
|
80
|
+
disconnecting the connector in Claude / ChatGPT settings, or by running <code>npx webcake-storefront-mcp logout</code>.
|
|
81
|
+
Storefront content you create lives in your WebCake account and is managed there. To request deletion of
|
|
82
|
+
anything else, contact us below.</p>
|
|
83
|
+
|
|
84
|
+
<h2>Security</h2>
|
|
85
|
+
<p>All traffic uses HTTPS. The connector implements OAuth 2.1 with PKCE; your raw WebCake token is never
|
|
86
|
+
exposed to the AI assistant — only resolved server-side per request via an opaque OAuth access token.</p>
|
|
87
|
+
|
|
88
|
+
<h2>Contact</h2>
|
|
89
|
+
<p>Questions or requests: <a href="mailto:${CONTACT_EMAIL}">${CONTACT_EMAIL}</a> or open an issue at
|
|
90
|
+
<a href="https://github.com/vuluu2k/webcake-storefront-mcp/issues">github.com/vuluu2k/webcake-storefront-mcp</a>.</p>`);
|
|
91
|
+
}
|
|
92
|
+
export function termsHtml() {
|
|
93
|
+
return page("Terms of Service", `<h1>Terms of Service</h1>
|
|
94
|
+
<div class="meta">Last updated: ${LAST_UPDATED}</div>
|
|
95
|
+
<p>By connecting to and using the WebCake Storefront MCP connector ("the service") you agree to these terms.</p>
|
|
96
|
+
|
|
97
|
+
<h2>What the service does</h2>
|
|
98
|
+
<p>The service exposes tools that let an AI assistant create, update, and manage storefront pages, products,
|
|
99
|
+
articles, customers, and orders in your WebCake / StoreCake account. It acts on your behalf using credentials
|
|
100
|
+
you authorize through the WebCake OAuth login flow.</p>
|
|
101
|
+
|
|
102
|
+
<h2>Your responsibilities</h2>
|
|
103
|
+
<ul>
|
|
104
|
+
<li>You must have a valid WebCake / StoreCake account and the necessary permissions for any sites you target.</li>
|
|
105
|
+
<li>You are responsible for all content you generate and publish through the service, and for complying with
|
|
106
|
+
WebCake's terms of service and applicable law.</li>
|
|
107
|
+
<li>Do not use the service to create, distribute, or publish unlawful, infringing, or harmful content.</li>
|
|
108
|
+
<li>Do not attempt to use the service to access accounts or data you are not authorized to access.</li>
|
|
109
|
+
</ul>
|
|
110
|
+
|
|
111
|
+
<h2>Availability & changes</h2>
|
|
112
|
+
<p>The service is provided "as is" without warranty of any kind. We may update, suspend, or discontinue it at
|
|
113
|
+
any time and may change these terms; continued use after a change constitutes acceptance of the new terms.</p>
|
|
114
|
+
|
|
115
|
+
<h2>Limitation of liability</h2>
|
|
116
|
+
<p>To the fullest extent permitted by applicable law, the operators of the connector are not liable for
|
|
117
|
+
indirect, incidental, or consequential damages arising from use of the service. The service relies on
|
|
118
|
+
third-party platforms (WebCake, the AI assistant host) whose own terms also apply.</p>
|
|
119
|
+
|
|
120
|
+
<h2>Intellectual property</h2>
|
|
121
|
+
<p>The connector is open-source software released under the MIT License. The WebCake trademarks and platform
|
|
122
|
+
are owned by their respective holders.</p>
|
|
123
|
+
|
|
124
|
+
<h2>Contact</h2>
|
|
125
|
+
<p><a href="mailto:${CONTACT_EMAIL}">${CONTACT_EMAIL}</a> ·
|
|
126
|
+
<a href="https://github.com/vuluu2k/webcake-storefront-mcp/issues">GitHub Issues</a></p>`);
|
|
127
|
+
}
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marketing landing page for the WebCake Storefront MCP connector.
|
|
3
|
+
*
|
|
4
|
+
* Served at GET / when the client sends Accept: text/html or is a known bot UA.
|
|
5
|
+
* Programmatic requests (healthcheck probes, MCP clients) still get JSON.
|
|
6
|
+
*/
|
|
7
|
+
const GITHUB_URL = "https://github.com/vuluu2k/webcake-storefront-mcp";
|
|
8
|
+
const NPM_URL = "https://www.npmjs.com/package/webcake-storefront-mcp";
|
|
9
|
+
const WEBCAKE_URL = "https://webcake.io";
|
|
10
|
+
const FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
11
|
+
<rect width="32" height="32" rx="7" fill="url(#g)"/>
|
|
12
|
+
<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
13
|
+
<stop offset="0%" stop-color="#3FBB57"/>
|
|
14
|
+
<stop offset="100%" stop-color="#108B67"/>
|
|
15
|
+
</linearGradient></defs>
|
|
16
|
+
<text x="16" y="22" text-anchor="middle" font-family="system-ui,sans-serif" font-weight="700" font-size="17" fill="white">S</text>
|
|
17
|
+
<circle cx="24" cy="9" r="4" fill="#FFD591"/>
|
|
18
|
+
</svg>`;
|
|
19
|
+
export function faviconSvg() {
|
|
20
|
+
return FAVICON_SVG;
|
|
21
|
+
}
|
|
22
|
+
export function landingHtml(origin = "") {
|
|
23
|
+
const mcpUrl = `${origin}/mcp`;
|
|
24
|
+
const npxCmd = `npx webcake-storefront-mcp@latest`;
|
|
25
|
+
return `<!doctype html>
|
|
26
|
+
<html lang="en">
|
|
27
|
+
<head>
|
|
28
|
+
<meta charset="utf-8">
|
|
29
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
30
|
+
<title>WebCake Storefront MCP — Build storefronts with AI</title>
|
|
31
|
+
<meta name="description" content="An MCP server that exposes ~101 tools for building and managing WebCake / StoreCake storefronts from any AI assistant — Claude, Cursor, Windsurf, and more.">
|
|
32
|
+
<meta property="og:type" content="website">
|
|
33
|
+
<meta property="og:title" content="WebCake Storefront MCP">
|
|
34
|
+
<meta property="og:description" content="Build, edit, and publish storefront pages, products, articles, and more — straight from your AI assistant.">
|
|
35
|
+
<meta property="og:image" content="${origin}/favicon.svg">
|
|
36
|
+
<meta property="og:url" content="${origin}">
|
|
37
|
+
<meta name="twitter:card" content="summary">
|
|
38
|
+
<meta name="twitter:title" content="WebCake Storefront MCP">
|
|
39
|
+
<meta name="twitter:description" content="~101 MCP tools for AI-powered storefront building on WebCake / StoreCake.">
|
|
40
|
+
<meta name="twitter:image" content="${origin}/favicon.svg">
|
|
41
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
42
|
+
<style>
|
|
43
|
+
:root {
|
|
44
|
+
--green: #108B67;
|
|
45
|
+
--green-light: #3FBB57;
|
|
46
|
+
--accent: #FFD591;
|
|
47
|
+
--bg: #f8fafc;
|
|
48
|
+
--bg2: #f1f5f9;
|
|
49
|
+
--text: #1e293b;
|
|
50
|
+
--muted: #64748b;
|
|
51
|
+
--border: rgba(0,0,0,.08);
|
|
52
|
+
--card: #fff;
|
|
53
|
+
color-scheme: light dark;
|
|
54
|
+
}
|
|
55
|
+
@media (prefers-color-scheme: dark) {
|
|
56
|
+
:root {
|
|
57
|
+
--bg: #0f172a;
|
|
58
|
+
--bg2: #1e293b;
|
|
59
|
+
--text: #e2e8f0;
|
|
60
|
+
--muted: #94a3b8;
|
|
61
|
+
--border: rgba(255,255,255,.08);
|
|
62
|
+
--card: #1e293b;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
66
|
+
body{font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;background:var(--bg);color:var(--text);line-height:1.6}
|
|
67
|
+
a{color:var(--green);text-decoration:none}
|
|
68
|
+
a:hover{text-decoration:underline}
|
|
69
|
+
.container{max-width:900px;margin:0 auto;padding:0 24px}
|
|
70
|
+
|
|
71
|
+
/* Nav */
|
|
72
|
+
nav{border-bottom:1px solid var(--border);padding:14px 0}
|
|
73
|
+
.nav-inner{display:flex;align-items:center;justify-content:space-between}
|
|
74
|
+
.nav-logo{display:flex;align-items:center;gap:10px;font-weight:700;font-size:1.05rem;color:var(--text);text-decoration:none}
|
|
75
|
+
.nav-logo svg{flex-shrink:0}
|
|
76
|
+
.nav-links{display:flex;align-items:center;gap:20px;font-size:.9rem}
|
|
77
|
+
.nav-links a{color:var(--muted)}
|
|
78
|
+
.nav-links a:hover{color:var(--text);text-decoration:none}
|
|
79
|
+
.btn{display:inline-flex;align-items:center;gap:6px;padding:8px 18px;border-radius:8px;font-size:.9rem;font-weight:600;cursor:pointer;border:none;transition:opacity .15s}
|
|
80
|
+
.btn-primary{background:var(--green);color:#fff}
|
|
81
|
+
.btn-primary:hover{opacity:.88;text-decoration:none}
|
|
82
|
+
.btn-outline{background:transparent;color:var(--text);border:1px solid var(--border)}
|
|
83
|
+
.btn-outline:hover{background:var(--bg2);text-decoration:none}
|
|
84
|
+
|
|
85
|
+
/* Hero */
|
|
86
|
+
.hero{padding:80px 0 64px;text-align:center}
|
|
87
|
+
.hero-badge{display:inline-flex;align-items:center;gap:6px;background:var(--bg2);border:1px solid var(--border);border-radius:99px;padding:5px 14px;font-size:.82rem;color:var(--muted);margin-bottom:28px}
|
|
88
|
+
.hero h1{font-size:clamp(2rem,5vw,3.2rem);font-weight:800;line-height:1.15;letter-spacing:-.02em;margin-bottom:20px}
|
|
89
|
+
.hero h1 span{color:var(--green)}
|
|
90
|
+
.hero p{font-size:1.15rem;color:var(--muted);max-width:580px;margin:0 auto 36px}
|
|
91
|
+
.hero-actions{display:flex;align-items:center;justify-content:center;gap:12px;flex-wrap:wrap}
|
|
92
|
+
.hero-mcp{margin-top:40px;background:var(--bg2);border:1px solid var(--border);border-radius:12px;padding:18px 24px;display:inline-block;text-align:left;max-width:620px;width:100%}
|
|
93
|
+
.hero-mcp label{font-size:.78rem;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;display:block;margin-bottom:8px}
|
|
94
|
+
.hero-mcp-row{display:flex;align-items:center;gap:10px}
|
|
95
|
+
.hero-mcp code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.95rem;flex:1;word-break:break-all;color:var(--text)}
|
|
96
|
+
.copy-btn{background:var(--card);border:1px solid var(--border);border-radius:6px;padding:5px 10px;font-size:.8rem;color:var(--muted);cursor:pointer;white-space:nowrap;transition:background .15s}
|
|
97
|
+
.copy-btn:hover{background:var(--bg2)}
|
|
98
|
+
|
|
99
|
+
/* Badges */
|
|
100
|
+
.badges{display:flex;align-items:center;justify-content:center;gap:8px;flex-wrap:wrap;margin:28px 0}
|
|
101
|
+
.badges img{height:20px}
|
|
102
|
+
|
|
103
|
+
/* Features */
|
|
104
|
+
.section{padding:56px 0}
|
|
105
|
+
.section-label{font-size:.82rem;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--green);margin-bottom:10px}
|
|
106
|
+
.section h2{font-size:clamp(1.5rem,3vw,2.1rem);font-weight:700;letter-spacing:-.01em;margin-bottom:12px}
|
|
107
|
+
.section p.lead{color:var(--muted);font-size:1.05rem;max-width:560px;margin-bottom:40px}
|
|
108
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:20px}
|
|
109
|
+
.card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:26px}
|
|
110
|
+
.card-icon{font-size:1.6rem;margin-bottom:12px}
|
|
111
|
+
.card h3{font-size:1rem;font-weight:700;margin-bottom:6px}
|
|
112
|
+
.card p{font-size:.9rem;color:var(--muted)}
|
|
113
|
+
|
|
114
|
+
/* Tools list */
|
|
115
|
+
.tools-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px}
|
|
116
|
+
.tool-chip{background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:9px 14px;font-size:.83rem;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text)}
|
|
117
|
+
|
|
118
|
+
/* Install */
|
|
119
|
+
.install-block{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:32px;margin-top:32px}
|
|
120
|
+
.install-block h3{font-size:1.1rem;font-weight:700;margin-bottom:6px}
|
|
121
|
+
.install-block p{color:var(--muted);font-size:.9rem;margin-bottom:18px}
|
|
122
|
+
.code-block{background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:16px 20px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.9rem;position:relative}
|
|
123
|
+
.code-block .copy-btn{position:absolute;top:10px;right:10px}
|
|
124
|
+
.install-tabs{display:flex;gap:6px;margin-bottom:16px;flex-wrap:wrap}
|
|
125
|
+
.tab-btn{background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:6px 14px;font-size:.85rem;cursor:pointer;color:var(--muted);transition:all .15s}
|
|
126
|
+
.tab-btn.active,.tab-btn:hover{background:var(--green);color:#fff;border-color:var(--green)}
|
|
127
|
+
|
|
128
|
+
/* Footer */
|
|
129
|
+
footer{border-top:1px solid var(--border);padding:40px 0;text-align:center;color:var(--muted);font-size:.88rem}
|
|
130
|
+
footer a{color:var(--muted)}
|
|
131
|
+
footer a:hover{color:var(--text)}
|
|
132
|
+
.footer-links{display:flex;align-items:center;justify-content:center;gap:20px;flex-wrap:wrap;margin-bottom:12px}
|
|
133
|
+
|
|
134
|
+
@media(max-width:600px){
|
|
135
|
+
.hero{padding:52px 0 40px}
|
|
136
|
+
.hero-mcp-row{flex-direction:column;align-items:flex-start}
|
|
137
|
+
nav .btn-outline{display:none}
|
|
138
|
+
}
|
|
139
|
+
</style>
|
|
140
|
+
</head>
|
|
141
|
+
<body>
|
|
142
|
+
|
|
143
|
+
<nav>
|
|
144
|
+
<div class="container">
|
|
145
|
+
<div class="nav-inner">
|
|
146
|
+
<a class="nav-logo" href="/">
|
|
147
|
+
<svg width="28" height="28" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
|
148
|
+
<rect width="32" height="32" rx="7" fill="url(#ng)"/>
|
|
149
|
+
<defs><linearGradient id="ng" x1="0" y1="0" x2="1" y2="1">
|
|
150
|
+
<stop offset="0%" stop-color="#3FBB57"/><stop offset="100%" stop-color="#108B67"/>
|
|
151
|
+
</linearGradient></defs>
|
|
152
|
+
<text x="16" y="22" text-anchor="middle" font-family="system-ui,sans-serif" font-weight="700" font-size="17" fill="white">S</text>
|
|
153
|
+
<circle cx="24" cy="9" r="4" fill="#FFD591"/>
|
|
154
|
+
</svg>
|
|
155
|
+
WebCake Storefront MCP
|
|
156
|
+
</a>
|
|
157
|
+
<div class="nav-links">
|
|
158
|
+
<a href="${GITHUB_URL}" target="_blank" rel="noopener">GitHub</a>
|
|
159
|
+
<a href="/privacy">Privacy</a>
|
|
160
|
+
<a href="/terms">Terms</a>
|
|
161
|
+
<a class="btn btn-primary" href="${WEBCAKE_URL}" target="_blank" rel="noopener">Get WebCake</a>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</nav>
|
|
166
|
+
|
|
167
|
+
<main>
|
|
168
|
+
<div class="hero">
|
|
169
|
+
<div class="container">
|
|
170
|
+
<div class="hero-badge">
|
|
171
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
|
|
172
|
+
~101 tools · Model Context Protocol
|
|
173
|
+
</div>
|
|
174
|
+
<h1>Build your <span>storefront</span><br>from a prompt</h1>
|
|
175
|
+
<p>An MCP server that connects any AI assistant to your WebCake / StoreCake account — create pages, manage products, publish articles, and more without leaving your IDE.</p>
|
|
176
|
+
<div class="hero-actions">
|
|
177
|
+
<a class="btn btn-primary" href="${GITHUB_URL}" target="_blank" rel="noopener">
|
|
178
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.477 2 2 6.477 2 12c0 4.418 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.009-.868-.013-1.703-2.782.604-3.369-1.342-3.369-1.342-.454-1.155-1.11-1.462-1.11-1.462-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.268 2.75 1.026A9.578 9.578 0 0112 6.836a9.59 9.59 0 012.504.337c1.909-1.294 2.747-1.026 2.747-1.026.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.579.688.481C19.138 20.163 22 16.418 22 12c0-5.523-4.477-10-10-10z"/></svg>
|
|
179
|
+
GitHub
|
|
180
|
+
</a>
|
|
181
|
+
<a class="btn btn-outline" href="${NPM_URL}" target="_blank" rel="noopener">npm package</a>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<div class="badges">
|
|
185
|
+
<a href="${NPM_URL}" target="_blank" rel="noopener"><img src="https://img.shields.io/npm/v/webcake-storefront-mcp?label=npm&color=108B67" alt="npm version"></a>
|
|
186
|
+
<a href="${NPM_URL}" target="_blank" rel="noopener"><img src="https://img.shields.io/npm/dm/webcake-storefront-mcp?color=3FBB57" alt="npm downloads"></a>
|
|
187
|
+
<a href="${GITHUB_URL}/blob/main/LICENSE" target="_blank" rel="noopener"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT license"></a>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div class="hero-mcp">
|
|
191
|
+
<label>Remote MCP URL (paste into Claude / Cursor / Windsurf)</label>
|
|
192
|
+
<div class="hero-mcp-row">
|
|
193
|
+
<code id="mcp-url">${mcpUrl}</code>
|
|
194
|
+
<button class="copy-btn" onclick="copyText('mcp-url',this)">Copy</button>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<div class="section" style="background:var(--bg2);padding-top:56px;padding-bottom:56px">
|
|
201
|
+
<div class="container">
|
|
202
|
+
<div class="section-label">Why Storefront MCP</div>
|
|
203
|
+
<h2>Everything you need to run a storefront, as MCP tools</h2>
|
|
204
|
+
<p class="lead">Drop it into your AI IDE and describe what you want — the assistant calls the right tools automatically.</p>
|
|
205
|
+
<div class="grid">
|
|
206
|
+
<div class="card">
|
|
207
|
+
<div class="card-icon">🏗️</div>
|
|
208
|
+
<h3>Page builder</h3>
|
|
209
|
+
<p>Create, edit, and publish full storefront pages — sections, layout, custom CSS/JS, and global sections — from natural language.</p>
|
|
210
|
+
</div>
|
|
211
|
+
<div class="card">
|
|
212
|
+
<div class="card-icon">🛍️</div>
|
|
213
|
+
<h3>Product & collection management</h3>
|
|
214
|
+
<p>List collections, inspect schemas, query records, and manage the catalog that powers your store.</p>
|
|
215
|
+
</div>
|
|
216
|
+
<div class="card">
|
|
217
|
+
<div class="card-icon">📝</div>
|
|
218
|
+
<h3>Articles & content</h3>
|
|
219
|
+
<p>Full CRUD for blog articles — create, draft, update, publish, and delete — with full content control.</p>
|
|
220
|
+
</div>
|
|
221
|
+
<div class="card">
|
|
222
|
+
<div class="card-icon">⚙️</div>
|
|
223
|
+
<h3>CMS file & function editing</h3>
|
|
224
|
+
<p>Read, write, and deploy HTTP functions and CMS files directly. Debug and run functions in place.</p>
|
|
225
|
+
</div>
|
|
226
|
+
<div class="card">
|
|
227
|
+
<div class="card-icon">👥</div>
|
|
228
|
+
<h3>Customer & order data</h3>
|
|
229
|
+
<p>Look up customers by ID, phone, or email. Read order data. Send transactional emails via the automation API.</p>
|
|
230
|
+
</div>
|
|
231
|
+
<div class="card">
|
|
232
|
+
<div class="card-icon">🔐</div>
|
|
233
|
+
<h3>OAuth 2.1 + PKCE</h3>
|
|
234
|
+
<p>Secure per-user authentication — listed in the Claude Connectors Directory. No token in the URL required.</p>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<div class="section">
|
|
241
|
+
<div class="container">
|
|
242
|
+
<div class="section-label">Tools</div>
|
|
243
|
+
<h2>~101 tools across 6 categories</h2>
|
|
244
|
+
<p class="lead">Every tool is typed with Zod schemas and returns structured data the model can reason about.</p>
|
|
245
|
+
<div class="tools-grid">
|
|
246
|
+
<div class="tool-chip">list_pages</div>
|
|
247
|
+
<div class="tool-chip">get_page</div>
|
|
248
|
+
<div class="tool-chip">create_page</div>
|
|
249
|
+
<div class="tool-chip">update_page</div>
|
|
250
|
+
<div class="tool-chip">delete_page</div>
|
|
251
|
+
<div class="tool-chip">publish_page</div>
|
|
252
|
+
<div class="tool-chip">get_page_contents</div>
|
|
253
|
+
<div class="tool-chip">update_page_contents</div>
|
|
254
|
+
<div class="tool-chip">get_page_custom_code</div>
|
|
255
|
+
<div class="tool-chip">update_page_custom_code</div>
|
|
256
|
+
<div class="tool-chip">list_global_sections</div>
|
|
257
|
+
<div class="tool-chip">get_global_section</div>
|
|
258
|
+
<div class="tool-chip">list_cms_files</div>
|
|
259
|
+
<div class="tool-chip">get_http_function</div>
|
|
260
|
+
<div class="tool-chip">create_http_function</div>
|
|
261
|
+
<div class="tool-chip">update_http_function</div>
|
|
262
|
+
<div class="tool-chip">delete_http_function</div>
|
|
263
|
+
<div class="tool-chip">debug_http_function</div>
|
|
264
|
+
<div class="tool-chip">run_http_function</div>
|
|
265
|
+
<div class="tool-chip">list_collections</div>
|
|
266
|
+
<div class="tool-chip">get_collection_schema</div>
|
|
267
|
+
<div class="tool-chip">query_collection</div>
|
|
268
|
+
<div class="tool-chip">list_articles</div>
|
|
269
|
+
<div class="tool-chip">get_article</div>
|
|
270
|
+
<div class="tool-chip">create_article</div>
|
|
271
|
+
<div class="tool-chip">update_article</div>
|
|
272
|
+
<div class="tool-chip">delete_article</div>
|
|
273
|
+
<div class="tool-chip">get_customer</div>
|
|
274
|
+
<div class="tool-chip">send_email</div>
|
|
275
|
+
<div class="tool-chip">get_site_custom_code</div>
|
|
276
|
+
<div class="tool-chip">update_site_custom_code</div>
|
|
277
|
+
<div class="tool-chip">+ many more…</div>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
<div class="section" style="background:var(--bg2);padding-top:56px;padding-bottom:56px">
|
|
283
|
+
<div class="container">
|
|
284
|
+
<div class="section-label">Get started</div>
|
|
285
|
+
<h2>Two ways to connect</h2>
|
|
286
|
+
<p class="lead">Use the hosted remote URL for instant setup, or run locally via npx for full control.</p>
|
|
287
|
+
|
|
288
|
+
<div class="install-block">
|
|
289
|
+
<h3>Option 1 — Remote (recommended)</h3>
|
|
290
|
+
<p>Paste the MCP URL into your AI assistant. OAuth login handles authentication automatically.</p>
|
|
291
|
+
<div class="code-block">
|
|
292
|
+
<code id="remote-url">${mcpUrl}</code>
|
|
293
|
+
<button class="copy-btn" onclick="copyText('remote-url',this)">Copy</button>
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
<div class="install-block" style="margin-top:16px">
|
|
298
|
+
<h3>Option 2 — Local via npx</h3>
|
|
299
|
+
<p>Run the MCP server on your own machine. Great for development or offline use.</p>
|
|
300
|
+
<div class="install-tabs">
|
|
301
|
+
<button class="tab-btn active" onclick="showTab('npx',this)">npx</button>
|
|
302
|
+
<button class="tab-btn" onclick="showTab('login',this)">login flow</button>
|
|
303
|
+
<button class="tab-btn" onclick="showTab('env',this)">env vars</button>
|
|
304
|
+
</div>
|
|
305
|
+
<div id="tab-npx" class="code-block">
|
|
306
|
+
<code id="npx-cmd">${npxCmd} serve --port 3000</code>
|
|
307
|
+
<button class="copy-btn" onclick="copyText('npx-cmd',this)">Copy</button>
|
|
308
|
+
</div>
|
|
309
|
+
<div id="tab-login" class="code-block" style="display:none">
|
|
310
|
+
<code id="login-cmd">${npxCmd} login</code>
|
|
311
|
+
<button class="copy-btn" onclick="copyText('login-cmd',this)">Copy</button>
|
|
312
|
+
</div>
|
|
313
|
+
<div id="tab-env" class="code-block" style="display:none">
|
|
314
|
+
<code id="env-cmd">WEBCAKE_TOKEN=your_jwt WEBCAKE_SITE_ID=your_site ${npxCmd}</code>
|
|
315
|
+
<button class="copy-btn" onclick="copyText('env-cmd',this)">Copy</button>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
<div class="section">
|
|
322
|
+
<div class="container" style="text-align:center">
|
|
323
|
+
<div class="section-label">Open source</div>
|
|
324
|
+
<h2>MIT licensed, community-driven</h2>
|
|
325
|
+
<p class="lead" style="margin:0 auto 32px">Contributions welcome. Open an issue or PR on GitHub.</p>
|
|
326
|
+
<a class="btn btn-primary" href="${GITHUB_URL}" target="_blank" rel="noopener" style="margin:0 auto">View on GitHub</a>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
</main>
|
|
330
|
+
|
|
331
|
+
<footer>
|
|
332
|
+
<div class="container">
|
|
333
|
+
<div class="footer-links">
|
|
334
|
+
<a href="${GITHUB_URL}" target="_blank" rel="noopener">GitHub</a>
|
|
335
|
+
<a href="${NPM_URL}" target="_blank" rel="noopener">npm</a>
|
|
336
|
+
<a href="${WEBCAKE_URL}" target="_blank" rel="noopener">WebCake</a>
|
|
337
|
+
<a href="/privacy">Privacy Policy</a>
|
|
338
|
+
<a href="/terms">Terms of Service</a>
|
|
339
|
+
</div>
|
|
340
|
+
<p>WebCake Storefront MCP © ${new Date().getFullYear()} · MIT License</p>
|
|
341
|
+
</div>
|
|
342
|
+
</footer>
|
|
343
|
+
|
|
344
|
+
<script>
|
|
345
|
+
function copyText(id, btn) {
|
|
346
|
+
const el = document.getElementById(id);
|
|
347
|
+
if (!el) return;
|
|
348
|
+
navigator.clipboard.writeText(el.textContent || '').then(() => {
|
|
349
|
+
const orig = btn.textContent;
|
|
350
|
+
btn.textContent = 'Copied!';
|
|
351
|
+
setTimeout(() => { btn.textContent = orig; }, 1500);
|
|
352
|
+
}).catch(() => {
|
|
353
|
+
const r = document.createRange();
|
|
354
|
+
r.selectNodeContents(el);
|
|
355
|
+
window.getSelection()?.removeAllRanges();
|
|
356
|
+
window.getSelection()?.addRange(r);
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
function showTab(name, btn) {
|
|
360
|
+
['npx','login','env'].forEach(t => {
|
|
361
|
+
const el = document.getElementById('tab-' + t);
|
|
362
|
+
if (el) el.style.display = t === name ? '' : 'none';
|
|
363
|
+
});
|
|
364
|
+
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|
365
|
+
btn.classList.add('active');
|
|
366
|
+
}
|
|
367
|
+
</script>
|
|
368
|
+
</body>
|
|
369
|
+
</html>`;
|
|
370
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcake-storefront-mcp",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "MCP server for the WebCake/StoreCake storefront builder — page CRUD, page authoring, products, orders, and more",
|
|
5
5
|
"mcpName": "io.github.vuluu2k/webcake-storefront-mcp",
|
|
6
6
|
"license": "MIT",
|