@sellhapi/mcp 0.0.0-stage → 0.1.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
@@ -1,3 +1,143 @@
1
- # Temporary Holding Version
1
+ # @sellhapi/mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ MCP server for SellHapi — build and manage storefronts from AI tools.
4
+
5
+ ## Two modes
6
+
7
+ **Stdio** (Claude Code, Cursor, Windsurf) — runs locally, authenticates with API key.
8
+
9
+ **Hosted HTTP** (ChatGPT, Claude web) — runs as a service, authenticates via OAuth 2.1.
10
+
11
+ ## Setup — stdio
12
+
13
+ ### 1. Install
14
+
15
+ ```bash
16
+ npm install -g @sellhapi/mcp
17
+ ```
18
+
19
+ ### 2. Get an API key
20
+
21
+ Go to **sellhapi.com** → Settings → API Keys → Create Key.
22
+
23
+ ### 3. Configure your AI tool
24
+
25
+ **Claude Code** — add to `~/.claude/claude_desktop_config.json`:
26
+
27
+ ```json
28
+ {
29
+ "mcpServers": {
30
+ "sellhapi": {
31
+ "command": "sellhapi-mcp",
32
+ "args": ["--api-key", "sk_live_your_key_here"]
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ **Cursor** — add to MCP settings:
39
+
40
+ ```json
41
+ {
42
+ "mcpServers": {
43
+ "sellhapi": {
44
+ "command": "npx",
45
+ "args": ["-y", "@sellhapi/mcp", "--api-key", "sk_live_your_key_here"]
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ Or set the environment variable instead of `--api-key`:
52
+
53
+ ```bash
54
+ export SELLHAPI_API_KEY=sk_live_your_key_here
55
+ ```
56
+
57
+ ## Setup — hosted
58
+
59
+ ### 1. Build
60
+
61
+ ```bash
62
+ npm install
63
+ npm run build
64
+ ```
65
+
66
+ ### 2. Configure environment
67
+
68
+ ```bash
69
+ cp .env.example .env
70
+ # Edit .env with your values
71
+ ```
72
+
73
+ Required env vars:
74
+
75
+ | Variable | Description | Default |
76
+ |----------|-------------|---------|
77
+ | `PORT` | HTTP port | `3100` |
78
+ | `SELLHAPI_API_URL` | sell_api base URL | production |
79
+ | `MCP_SERVER_URL` | Public URL of this server (for OAuth metadata) | `http://localhost:PORT` |
80
+
81
+ ### 3. Run
82
+
83
+ ```bash
84
+ npm run start:hosted
85
+ ```
86
+
87
+ Endpoints:
88
+
89
+ - `POST /mcp` — MCP Streamable HTTP (initialize + messages)
90
+ - `GET /mcp` — SSE stream
91
+ - `DELETE /mcp` — session teardown
92
+ - `GET /health` — health check
93
+ - `/.well-known/oauth-authorization-server` — OAuth metadata
94
+
95
+ ### sell_api env vars
96
+
97
+ The sell_api server needs these for OAuth:
98
+
99
+ | Variable | Description |
100
+ |----------|-------------|
101
+ | `MCP_OAUTH_SECRET` | JWT signing secret for MCP access tokens. Falls back to `JWT_VENDOR_SECRET` if not set. |
102
+ | `SELLHAPI_VENDOR_URL` | Base URL for consent pages (default: `https://sellhapi.com`) |
103
+
104
+ ## Tools
105
+
106
+ ### Studio (5)
107
+ - `list_projects` — list vendor's Studio projects
108
+ - `get_project` — get project details with code
109
+ - `create_project` — create a new project
110
+ - `push_code` — push React code (validated by BuildGuard)
111
+ - `deploy_website` — publish project to live storefront
112
+
113
+ ### Commerce (8)
114
+ - `list_products`, `list_digital_products`, `list_service_listings`
115
+ - `list_events`, `list_membership_tiers`, `list_donation_campaigns`
116
+ - `list_appointment_services`, `list_capabilities`
117
+
118
+ ### Content (4)
119
+ - `list_content`, `list_forms`, `list_collections`, `list_articles`
120
+
121
+ ### Intelligence (2)
122
+ - `list_orders`, `get_wallet_balance`
123
+
124
+ ## Resources
125
+
126
+ - `sellhapi://runtime-hooks` — available React hooks from @sellhapi/studio-runtime
127
+ - `sellhapi://build-rules` — BuildGuard validation rules
128
+ - `sellhapi://capability-patterns` — UX patterns per capability
129
+
130
+ ## Auth
131
+
132
+ **API key** (`sk_live_...`) — for stdio clients. Create at sellhapi.com → Settings → API Keys.
133
+
134
+ **OAuth 2.1** — for web clients. The hosted server proxies OAuth to sell_api. Supports dynamic client registration (RFC 7591) and PKCE (S256).
135
+
136
+ ## Development
137
+
138
+ ```bash
139
+ npm run dev # watch mode (tsc --watch)
140
+ npm run build # compile
141
+ npm start # stdio mode
142
+ npm run start:hosted # hosted HTTP mode
143
+ ```
@@ -0,0 +1,33 @@
1
+ /**
2
+ * HTTP client for sell_api.
3
+ * All MCP tools delegate to this client for actual API calls.
4
+ *
5
+ * Supports two modes:
6
+ * 1. Module-level config (stdio) — single API key set once via configure()
7
+ * 2. Per-request context (hosted) — API key scoped via AsyncLocalStorage
8
+ *
9
+ * In hosted mode, withApiKey() wraps each request handler to scope the
10
+ * credentials. Tool code is identical in both modes — get/post/etc. resolve
11
+ * the active key automatically.
12
+ */
13
+ /** Set module-level credentials (stdio mode). */
14
+ export declare function configure(key: string, base?: string): void;
15
+ /**
16
+ * Run a callback with scoped API key credentials (hosted mode).
17
+ * All get/post/etc. calls inside the callback will use these credentials.
18
+ */
19
+ export declare function withApiKey<T>(apiKey: string, baseUrl: string | undefined, fn: () => T): T;
20
+ /** Error with full response body preserved for structured error handling. */
21
+ export declare class ApiError extends Error {
22
+ readonly status: number;
23
+ readonly body: any;
24
+ constructor(message: string, status: number, body: any);
25
+ }
26
+ export declare function apiCall<T = unknown>(method: "GET" | "POST" | "PATCH" | "DELETE", path: string, body?: unknown): Promise<T>;
27
+ /**
28
+ * Convenience wrappers.
29
+ */
30
+ export declare const get: <T = unknown>(path: string) => Promise<T>;
31
+ export declare const post: <T = unknown>(path: string, body?: unknown) => Promise<T>;
32
+ export declare const patch: <T = unknown>(path: string, body?: unknown) => Promise<T>;
33
+ export declare const del: <T = unknown>(path: string) => Promise<T>;
package/dist/client.js ADDED
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ /**
3
+ * HTTP client for sell_api.
4
+ * All MCP tools delegate to this client for actual API calls.
5
+ *
6
+ * Supports two modes:
7
+ * 1. Module-level config (stdio) — single API key set once via configure()
8
+ * 2. Per-request context (hosted) — API key scoped via AsyncLocalStorage
9
+ *
10
+ * In hosted mode, withApiKey() wraps each request handler to scope the
11
+ * credentials. Tool code is identical in both modes — get/post/etc. resolve
12
+ * the active key automatically.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.del = exports.patch = exports.post = exports.get = exports.ApiError = void 0;
16
+ exports.configure = configure;
17
+ exports.withApiKey = withApiKey;
18
+ exports.apiCall = apiCall;
19
+ const node_async_hooks_1 = require("node:async_hooks");
20
+ const DEFAULT_BASE_URL = "https://sell-api-xl2a.onrender.com";
21
+ /** AsyncLocalStorage for per-request API key scoping in hosted mode. */
22
+ const als = new node_async_hooks_1.AsyncLocalStorage();
23
+ /** Module-level fallback for stdio mode (single key). */
24
+ let globalCtx;
25
+ /** Set module-level credentials (stdio mode). */
26
+ function configure(key, base) {
27
+ globalCtx = {
28
+ apiKey: key,
29
+ baseUrl: (base ?? DEFAULT_BASE_URL).replace(/\/$/, ""),
30
+ };
31
+ }
32
+ /**
33
+ * Run a callback with scoped API key credentials (hosted mode).
34
+ * All get/post/etc. calls inside the callback will use these credentials.
35
+ */
36
+ function withApiKey(apiKey, baseUrl, fn) {
37
+ const ctx = {
38
+ apiKey,
39
+ baseUrl: (baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, ""),
40
+ };
41
+ return als.run(ctx, fn);
42
+ }
43
+ /** Resolve the active credentials (ALS first, then module-level). */
44
+ function getContext() {
45
+ const ctx = als.getStore() ?? globalCtx;
46
+ if (!ctx)
47
+ throw new Error("Client not configured — call configure() or use withApiKey()");
48
+ return ctx;
49
+ }
50
+ // ── API calls ────────────────────────────────────────────────────────────────
51
+ /** Error with full response body preserved for structured error handling. */
52
+ class ApiError extends Error {
53
+ status;
54
+ body;
55
+ constructor(message, status, body) {
56
+ super(message);
57
+ this.status = status;
58
+ this.body = body;
59
+ this.name = "ApiError";
60
+ }
61
+ }
62
+ exports.ApiError = ApiError;
63
+ async function apiCall(method, path, body) {
64
+ const { apiKey, baseUrl } = getContext();
65
+ const url = `${baseUrl}${path}`;
66
+ const headers = {
67
+ "Authorization": `Bearer ${apiKey}`,
68
+ "Content-Type": "application/json",
69
+ };
70
+ const res = await fetch(url, {
71
+ method,
72
+ headers,
73
+ body: body ? JSON.stringify(body) : undefined,
74
+ });
75
+ const text = await res.text();
76
+ let data;
77
+ try {
78
+ data = JSON.parse(text);
79
+ }
80
+ catch {
81
+ data = { raw: text };
82
+ }
83
+ if (!res.ok) {
84
+ const msg = data?.error ?? data?.message ?? `HTTP ${res.status}`;
85
+ throw new ApiError(msg, res.status, data);
86
+ }
87
+ return data;
88
+ }
89
+ /**
90
+ * Convenience wrappers.
91
+ */
92
+ const get = (path) => apiCall("GET", path);
93
+ exports.get = get;
94
+ const post = (path, body) => apiCall("POST", path, body);
95
+ exports.post = post;
96
+ const patch = (path, body) => apiCall("PATCH", path, body);
97
+ exports.patch = patch;
98
+ const del = (path) => apiCall("DELETE", path);
99
+ exports.del = del;
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @sellhapi/mcp — hosted HTTP entry point with OAuth 2.1.
4
+ *
5
+ * For web-based AI clients (ChatGPT, Claude web) that connect via Streamable HTTP.
6
+ * Supports two auth methods:
7
+ * 1. API key (Bearer sk_live_...) — directly scoped
8
+ * 2. OAuth (Bearer eyJ...) — user logs in via SellHapi, AI client gets token
9
+ *
10
+ * The OAuth flow uses the MCP SDK's ProxyOAuthServerProvider which delegates
11
+ * all auth decisions to sell_api's OAuth endpoints. This server is stateless
12
+ * with respect to auth — it just proxies.
13
+ *
14
+ * Environment variables:
15
+ * PORT — HTTP port (default 3100)
16
+ * SELLHAPI_API_URL — sell_api base URL (default production)
17
+ * MCP_SERVER_URL — public URL of this MCP server (for OAuth metadata)
18
+ *
19
+ * Endpoints:
20
+ * POST/GET/DELETE /mcp — MCP Streamable HTTP
21
+ * GET /health — Health check
22
+ * /.well-known/* — OAuth metadata (auto-mounted by mcpAuthRouter)
23
+ * /authorize, /token, /register — OAuth (proxied to sell_api)
24
+ */
25
+ export {};
package/dist/hosted.js ADDED
@@ -0,0 +1,234 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * @sellhapi/mcp — hosted HTTP entry point with OAuth 2.1.
5
+ *
6
+ * For web-based AI clients (ChatGPT, Claude web) that connect via Streamable HTTP.
7
+ * Supports two auth methods:
8
+ * 1. API key (Bearer sk_live_...) — directly scoped
9
+ * 2. OAuth (Bearer eyJ...) — user logs in via SellHapi, AI client gets token
10
+ *
11
+ * The OAuth flow uses the MCP SDK's ProxyOAuthServerProvider which delegates
12
+ * all auth decisions to sell_api's OAuth endpoints. This server is stateless
13
+ * with respect to auth — it just proxies.
14
+ *
15
+ * Environment variables:
16
+ * PORT — HTTP port (default 3100)
17
+ * SELLHAPI_API_URL — sell_api base URL (default production)
18
+ * MCP_SERVER_URL — public URL of this MCP server (for OAuth metadata)
19
+ *
20
+ * Endpoints:
21
+ * POST/GET/DELETE /mcp — MCP Streamable HTTP
22
+ * GET /health — Health check
23
+ * /.well-known/* — OAuth metadata (auto-mounted by mcpAuthRouter)
24
+ * /authorize, /token, /register — OAuth (proxied to sell_api)
25
+ */
26
+ var __importDefault = (this && this.__importDefault) || function (mod) {
27
+ return (mod && mod.__esModule) ? mod : { "default": mod };
28
+ };
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ const express_1 = __importDefault(require("express"));
31
+ const node_crypto_1 = require("node:crypto");
32
+ const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
33
+ const router_js_1 = require("@modelcontextprotocol/sdk/server/auth/router.js");
34
+ const proxyProvider_js_1 = require("@modelcontextprotocol/sdk/server/auth/providers/proxyProvider.js");
35
+ const client_js_1 = require("./client.js");
36
+ const server_js_1 = require("./server.js");
37
+ const PORT = parseInt(process.env.PORT ?? "3100", 10);
38
+ const API_URL = process.env.SELLHAPI_API_URL ?? "https://sell-api-xl2a.onrender.com";
39
+ const SERVER_URL = process.env.MCP_SERVER_URL ?? `http://localhost:${PORT}`;
40
+ const sessions = new Map();
41
+ const SESSION_TTL_MS = 30 * 60 * 1000;
42
+ const MAX_SESSIONS = 1000;
43
+ setInterval(() => {
44
+ const now = Date.now();
45
+ for (const [id, session] of sessions) {
46
+ if (now - session.lastUsed > SESSION_TTL_MS) {
47
+ session.transport.close?.();
48
+ sessions.delete(id);
49
+ }
50
+ }
51
+ }, 5 * 60 * 1000).unref();
52
+ // ── OAuth provider (proxies to sell_api) ─────────────────────────────────────
53
+ const oauthProvider = new proxyProvider_js_1.ProxyOAuthServerProvider({
54
+ endpoints: {
55
+ authorizationUrl: `${API_URL}/api/mcp/oauth/authorize`,
56
+ tokenUrl: `${API_URL}/api/mcp/oauth/token`,
57
+ registrationUrl: `${API_URL}/api/mcp/oauth/register`,
58
+ },
59
+ async verifyAccessToken(token) {
60
+ const res = await fetch(`${API_URL}/api/mcp/oauth/verify-token`, {
61
+ method: "POST",
62
+ headers: { "Content-Type": "application/json" },
63
+ body: JSON.stringify({ token }),
64
+ });
65
+ if (!res.ok) {
66
+ throw new Error("Invalid or expired token");
67
+ }
68
+ const data = await res.json();
69
+ if (!data.vendorId) {
70
+ throw new Error("Invalid token — missing vendorId");
71
+ }
72
+ return {
73
+ token,
74
+ clientId: data.clientId,
75
+ scopes: data.scope ? data.scope.split(" ") : [],
76
+ expiresAt: data.expiresAt,
77
+ extra: { vendorId: data.vendorId },
78
+ };
79
+ },
80
+ async getClient(clientId) {
81
+ const res = await fetch(`${API_URL}/api/mcp/oauth/client/${encodeURIComponent(clientId)}`);
82
+ if (!res.ok)
83
+ return undefined;
84
+ const data = await res.json();
85
+ return data;
86
+ },
87
+ });
88
+ // ── Resolve API key from auth context ────────────────────────────────────────
89
+ function resolveApiKeyFromAuth(req) {
90
+ const auth = req.headers.authorization;
91
+ if (!auth)
92
+ return null;
93
+ const token = auth.replace(/^Bearer\s+/i, "");
94
+ // API key path — pass through directly
95
+ if (token.startsWith("sk_live_")) {
96
+ return token;
97
+ }
98
+ // OAuth path — the authInfo was set by mcpAuthRouter's bearer middleware
99
+ // We use the token itself as the "API key" for the HTTP client
100
+ // The sell_api apiKeyAuth middleware accepts both sk_live_ and JWT tokens
101
+ return token;
102
+ }
103
+ // ── Express app ──────────────────────────────────────────────────────────────
104
+ const app = (0, express_1.default)();
105
+ app.set("trust proxy", 1); // Render runs behind a reverse proxy
106
+ // CORS
107
+ app.use((req, res, next) => {
108
+ res.setHeader("Access-Control-Allow-Origin", "*");
109
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
110
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id");
111
+ res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
112
+ if (req.method === "OPTIONS") {
113
+ res.sendStatus(204);
114
+ return;
115
+ }
116
+ next();
117
+ });
118
+ // Health check
119
+ app.get("/health", (_req, res) => {
120
+ res.json({ status: "ok", sessions: sessions.size });
121
+ });
122
+ // OAuth routes (metadata, authorize, token, register)
123
+ // Must be mounted at root for .well-known discovery to work
124
+ app.use((0, router_js_1.mcpAuthRouter)({
125
+ provider: oauthProvider,
126
+ issuerUrl: new URL(SERVER_URL),
127
+ scopesSupported: ["studio", "commerce", "content", "intelligence"],
128
+ serviceDocumentationUrl: new URL("https://sellhapi.com/mcp"),
129
+ }));
130
+ // MCP endpoint — JSON body parsing
131
+ app.use("/mcp", express_1.default.json());
132
+ // MCP POST — initialize or send message
133
+ app.post("/mcp", async (req, res) => {
134
+ const apiKey = resolveApiKeyFromAuth(req);
135
+ if (!apiKey) {
136
+ res.status(401).json({ error: "Missing authorization" });
137
+ return;
138
+ }
139
+ const parsedBody = req.body;
140
+ const sessionId = req.headers["mcp-session-id"];
141
+ const isInit = Array.isArray(parsedBody)
142
+ ? parsedBody.some((m) => m.method === "initialize")
143
+ : parsedBody?.method === "initialize";
144
+ if (isInit) {
145
+ if (sessions.size >= MAX_SESSIONS) {
146
+ res.status(429).json({ error: "Too many active sessions. Try again later." });
147
+ return;
148
+ }
149
+ const server = (0, server_js_1.createMcpServer)();
150
+ const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
151
+ sessionIdGenerator: () => (0, node_crypto_1.randomUUID)(),
152
+ onsessioninitialized: (sid) => {
153
+ console.log(`[mcp] session initialized: ${sid}`);
154
+ sessions.set(sid, { server, transport, apiKey, lastUsed: Date.now() });
155
+ },
156
+ });
157
+ transport.onclose = () => {
158
+ if (transport.sessionId)
159
+ sessions.delete(transport.sessionId);
160
+ };
161
+ try {
162
+ await server.connect(transport);
163
+ await (0, client_js_1.withApiKey)(apiKey, API_URL, () => transport.handleRequest(req, res, parsedBody));
164
+ }
165
+ catch (err) {
166
+ console.error("[mcp] init error:", err);
167
+ if (!res.headersSent)
168
+ res.status(500).json({ error: "Internal server error" });
169
+ }
170
+ return;
171
+ }
172
+ if (!sessionId || !sessions.has(sessionId)) {
173
+ console.log(`[mcp] unknown session: ${sessionId}, active sessions: ${sessions.size}`);
174
+ res.status(400).json({ error: "Missing or invalid Mcp-Session-Id. Initialize first." });
175
+ return;
176
+ }
177
+ const session = sessions.get(sessionId);
178
+ session.lastUsed = Date.now();
179
+ const method = Array.isArray(parsedBody)
180
+ ? parsedBody.map((m) => m.method).join(",")
181
+ : parsedBody?.method;
182
+ console.log(`[mcp] ${sessionId.slice(0, 8)}… method=${method}`);
183
+ try {
184
+ await (0, client_js_1.withApiKey)(session.apiKey, API_URL, () => session.transport.handleRequest(req, res, parsedBody));
185
+ }
186
+ catch (err) {
187
+ console.error(`[mcp] ${sessionId.slice(0, 8)}… error:`, err);
188
+ if (!res.headersSent)
189
+ res.status(500).json({ error: "Internal server error" });
190
+ }
191
+ });
192
+ // MCP GET — SSE stream
193
+ app.get("/mcp", async (req, res) => {
194
+ const sessionId = req.headers["mcp-session-id"];
195
+ if (!sessionId || !sessions.has(sessionId)) {
196
+ res.status(400).json({ error: "Missing or invalid Mcp-Session-Id." });
197
+ return;
198
+ }
199
+ const session = sessions.get(sessionId);
200
+ session.lastUsed = Date.now();
201
+ try {
202
+ await (0, client_js_1.withApiKey)(session.apiKey, API_URL, () => session.transport.handleRequest(req, res));
203
+ }
204
+ catch (err) {
205
+ console.error(`[mcp] GET stream error:`, err);
206
+ if (!res.headersSent)
207
+ res.status(500).json({ error: "Internal server error" });
208
+ }
209
+ });
210
+ // MCP DELETE — session teardown
211
+ app.delete("/mcp", (req, res) => {
212
+ const sessionId = req.headers["mcp-session-id"];
213
+ if (sessionId && sessions.has(sessionId)) {
214
+ const session = sessions.get(sessionId);
215
+ session.transport.close?.();
216
+ sessions.delete(sessionId);
217
+ }
218
+ res.sendStatus(204);
219
+ });
220
+ // ── Keep-alive ping (prevent Render free tier sleep) ────────────────────────
221
+ // Ping sell_api every 10 minutes to keep it warm for tool calls
222
+ setInterval(async () => {
223
+ try {
224
+ await fetch(API_URL);
225
+ }
226
+ catch { /* ignore */ }
227
+ }, 10 * 60 * 1000).unref();
228
+ // ── Start ────────────────────────────────────────────────────────────────────
229
+ app.listen(PORT, () => {
230
+ console.log(`SellHapi MCP server (hosted + OAuth) listening on port ${PORT}`);
231
+ console.log(` MCP endpoint: ${SERVER_URL}/mcp`);
232
+ console.log(` OAuth issuer: ${SERVER_URL}`);
233
+ console.log(` Health check: ${SERVER_URL}/health`);
234
+ });
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @sellhapi/mcp — stdio entry point.
4
+ *
5
+ * For local AI tools (Claude Code, Cursor, etc.) that connect via stdio.
6
+ *
7
+ * Usage:
8
+ * npx @sellhapi/mcp --api-key sk_live_...
9
+ * npx @sellhapi/mcp --api-key sk_live_... --base-url http://localhost:5000
10
+ */
11
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * @sellhapi/mcp — stdio entry point.
5
+ *
6
+ * For local AI tools (Claude Code, Cursor, etc.) that connect via stdio.
7
+ *
8
+ * Usage:
9
+ * npx @sellhapi/mcp --api-key sk_live_...
10
+ * npx @sellhapi/mcp --api-key sk_live_... --base-url http://localhost:5000
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
14
+ const client_js_1 = require("./client.js");
15
+ const server_js_1 = require("./server.js");
16
+ // ── Parse CLI args ───────────────────────────────────────────────────────────
17
+ function parseArgs() {
18
+ const args = process.argv.slice(2);
19
+ let apiKey;
20
+ let baseUrl;
21
+ for (let i = 0; i < args.length; i++) {
22
+ if (args[i] === "--api-key" && args[i + 1]) {
23
+ apiKey = args[++i];
24
+ }
25
+ else if (args[i] === "--base-url" && args[i + 1]) {
26
+ baseUrl = args[++i];
27
+ }
28
+ else if (args[i].startsWith("--api-key=")) {
29
+ apiKey = args[i].split("=")[1];
30
+ }
31
+ else if (args[i].startsWith("--base-url=")) {
32
+ baseUrl = args[i].split("=")[1];
33
+ }
34
+ }
35
+ apiKey = apiKey ?? process.env.SELLHAPI_API_KEY;
36
+ baseUrl = baseUrl ?? process.env.SELLHAPI_BASE_URL;
37
+ if (!apiKey) {
38
+ console.error("Error: API key required. Use --api-key sk_live_... or set SELLHAPI_API_KEY env var.");
39
+ process.exit(1);
40
+ }
41
+ return { apiKey, baseUrl };
42
+ }
43
+ // ── Main ─────────────────────────────────────────────────────────────────────
44
+ async function main() {
45
+ const { apiKey, baseUrl } = parseArgs();
46
+ (0, client_js_1.configure)(apiKey, baseUrl);
47
+ const server = (0, server_js_1.createMcpServer)();
48
+ const transport = new stdio_js_1.StdioServerTransport();
49
+ await server.connect(transport);
50
+ }
51
+ main().catch((err) => {
52
+ console.error("Fatal:", err);
53
+ process.exit(1);
54
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * BuildGuard rules — extracted from buildGuard.service.ts.
3
+ * Code that violates these rules will be rejected by push_code.
4
+ */
5
+ export declare const BUILD_RULES = "\n# SellHapi BuildGuard Rules\n\nYour code MUST pass all these rules or push_code will reject it with specific violation details.\n\n## APPROVED IMPORTS (only these are allowed)\n\nExact matches:\n @sellhapi/studio-runtime\n react\n react-dom\n react-dom/client\n react/jsx-runtime\n react/jsx-dev-runtime\n framer-motion\n clsx\n date-fns\n zod\n lucide-react\n\nSubpath imports allowed:\n date-fns/*\n react-dom/*\n react/*\n\nRelative imports (./ and ../) are always allowed between project files.\n\nEVERYTHING ELSE IS BLOCKED \u2014 no axios, no tailwindcss, no lodash, no node builtins.\n\n## HARD VIOLATIONS (build fails)\n\n1. DISALLOWED IMPORTS\n Any import not in the approved list above.\n Violation: \"Import from X is not allowed.\"\n\n2. DYNAMIC REQUIRE\n require() with non-string argument or unapproved package.\n Violation: \"Dynamic require() is not allowed.\"\n\n3. DYNAMIC IMPORT\n import() expressions (as opposed to static import statements).\n Violation: \"Dynamic import() is not allowed in generated storefront code.\"\n\n4. CODE INJECTION\n eval(), new Function(), Function() \u2014 all blocked.\n Violation: \"eval()/Function() is not allowed \u2014 code injection risk.\"\n\n5. RAW NETWORK ACCESS\n fetch(), new XMLHttpRequest(), new WebSocket() \u2014 all blocked.\n Violation: \"Direct fetch() calls are not allowed. Use hooks from @sellhapi/studio-runtime instead.\"\n All data must flow through runtime hooks.\n\n6. DANGEROUS GLOBALS\n Blocked property access:\n - process.env\n - document.cookie, document.write(), document.writeln(), document.domain, document.execCommand()\n - window.fetch, window.eval, window.Function\n - window.XMLHttpRequest, window.WebSocket\n - window.localStorage, window.sessionStorage, window.indexedDB\n - navigator.sendBeacon\n - Standalone: localStorage, sessionStorage, indexedDB\n Violation: \"X is not allowed in storefront code.\"\n\n7. SUBSCRIPT ACCESS ON GLOBALS\n window[\"fetch\"], globalThis[\"eval\"], self[\"XMLHttpRequest\"] \u2014 all blocked.\n Violation: \"Subscript access on 'window' is not allowed.\"\n\n8. DANGEROUS JSX\n dangerouslySetInnerHTML \u2014 blocked.\n Violation: \"dangerouslySetInnerHTML is not allowed \u2014 XSS vector.\"\n\n9. MISSING CART DRAWER\n If any file uses useCart or addItem, <CartDrawer /> MUST be rendered somewhere.\n Violation: \"useCart/addItem is used but <CartDrawer /> is not rendered anywhere.\"\n\n## SOFT WARNINGS (build passes, but flagged)\n\n1. product.images[N].url \u2014 WRONG. images is string[], each element IS the URL.\n Fix: use images[N] directly or product.imageUrl for primary.\n\n2. addItem() without type field \u2014 CartItem.type is required.\n Fix: add type: 'physical' | 'digital' to every addItem() call.\n\n3. campaign.raised / campaign.goalAmount \u2014 WRONG field names.\n Fix: use campaign.raisedAmountKobo / campaign.goalAmountKobo (divide by 100 for display).\n\n4. ticketType.soldCount \u2014 WRONG. Use ticketType.sold.\n\n## REQUIRED: data-region ATTRIBUTES\n\nEvery section that renders live data MUST have a data-region attribute on its outermost container element.\nThis is required for Studio's region selector, Layers panel, and entity injection to work.\n\nRequired attributes:\n data-region=\"<slug>\" \u2014 unique kebab-case identifier, e.g. \"hero\", \"products\", \"appointments\"\n data-label=\"<name>\" \u2014 human-readable label shown in Studio UI, e.g. \"Products Section\"\n data-scope=\"content\" \u2014 always \"content\" for data-driven sections\n\nExample:\n <section data-region=\"products\" data-label=\"Products\" data-scope=\"content\">\n {/* useProducts() renders here */}\n </section>\n\n <section data-region=\"appointments\" data-label=\"Consultations\" data-scope=\"content\">\n {/* useServices() renders here */}\n </section>\n\nALSO add data-region to static layout sections (hero, footer, nav) \u2014 these don't need data hooks but must still be selectable:\n <section data-region=\"hero\" data-label=\"Hero\" data-scope=\"layout\">\n <header data-region=\"navigation\" data-label=\"Navigation\" data-scope=\"navigation\">\n <footer data-region=\"footer\" data-label=\"Footer\" data-scope=\"layout\">\n\nIf you use ANY data hook (useProducts, useServices, useEvents, etc.) without ANY data-region attributes, the build will warn you.\n\n## FILE STRUCTURE\n\nRequired: App.tsx (or App.jsx) as entry point.\nRecommended structure:\n App.tsx \u2014 main component with routing\n components/ \u2014 reusable UI components\n pages/ \u2014 route-level pages (ProductPage, EventPage, etc.)\n styles/ \u2014 optional shared styles\n\n## BUNDLE SIZE LIMITS\n\nHard limit: 4 MB (build fails above this)\nWarning: 3.5 MB (runtime baseline is ~2.1 MB \u2014 aim to keep user code under 1.4 MB)\n\n## STYLING\n\nUse inline styles (React style objects) \u2014 this is the convention.\nCSS modules work but are not typical.\nTailwind is NOT available (not in approved imports).\nframer-motion is available for animations.\n\n## KEY PATTERNS\n\n1. All data comes from hooks. NEVER hardcode product names, prices, or content.\n2. Show loading states: if (loading) return <div>Loading...</div>\n3. Show empty states: if (data.length === 0) return <div>No items yet</div>\n4. Format prices: (priceKobo / 100).toLocaleString() for Naira display\n5. Use useHashRouter() for multi-page navigation\n6. Commerce actions return { paymentLink } \u2014 redirect: window.location.href = result.paymentLink\n";