@01.software/cli 0.7.1 → 0.8.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.
@@ -1,5 +1,11 @@
1
1
  // src/handler.ts
2
2
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3
+ import {
4
+ MCP_OAUTH_ISSUER as MCP_OAUTH_ISSUER3,
5
+ MCP_PROTECTED_RESOURCE_METADATA_PATH,
6
+ MCP_RESOURCE_AUDIENCE as MCP_RESOURCE_AUDIENCE2,
7
+ MCP_SCOPES as MCP_SCOPES2
8
+ } from "@01.software/auth-contracts";
3
9
 
4
10
  // src/server.ts
5
11
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -10,40 +16,207 @@ import { z } from "zod";
10
16
  // src/lib/request-context.ts
11
17
  import { AsyncLocalStorage } from "async_hooks";
12
18
  var requestContext = new AsyncLocalStorage();
13
- function headers() {
14
- const ctx = requestContext.getStore();
15
- if (!ctx) return null;
16
- return Object.fromEntries(ctx.headers.entries());
19
+ function tenantAuthContext() {
20
+ return requestContext.getStore()?.auth ?? null;
21
+ }
22
+ function hasRequestContext() {
23
+ return requestContext.getStore() !== void 0;
17
24
  }
18
25
 
19
26
  // src/lib/client.ts
20
- import { createServerClient } from "@01.software/sdk";
21
- function getClient() {
22
- let secretKey;
23
- let publishableKey;
24
- try {
25
- const h = headers();
26
- secretKey = h?.["x-api-key"];
27
- publishableKey = h?.["x-publishable-key"] ?? h?.["x-client-key"];
28
- } catch {
27
+ import {
28
+ CollectionClient,
29
+ CommunityClient,
30
+ ModerationApi,
31
+ ServerCommerceClient,
32
+ createServerClient
33
+ } from "@01.software/sdk";
34
+
35
+ // src/service-auth.ts
36
+ import { createPrivateKey, randomUUID, sign as signBytes } from "crypto";
37
+ import {
38
+ MCP_CONSOLE_SERVICE_AUDIENCE,
39
+ MCP_CONSOLE_SERVICE_SCOPE,
40
+ MCP_OAUTH_ISSUER,
41
+ MCP_SERVICE_TOKEN_LIFETIME_SECONDS,
42
+ MCP_TENANT_CLAIM,
43
+ MCP_TENANT_ROLE_CLAIM
44
+ } from "@01.software/auth-contracts";
45
+ var KEYSET_ENV = "MCP_SERVICE_KEYSET";
46
+ function assertProductionKeysetUse(source) {
47
+ const vercelEnv = process.env.VERCEL_ENV;
48
+ if (vercelEnv && vercelEnv !== "production") {
49
+ throw new Error(
50
+ `${source} is only allowed in production Vercel deployments; non-production MCP service auth needs environment-specific issuer, audience, JWKS URI, and key material`
51
+ );
29
52
  }
30
- if (!secretKey) {
31
- secretKey = process.env.SOFTWARE_SECRET_KEY;
53
+ }
54
+ function parsePrivateJwk() {
55
+ const keyset = signingKeyset();
56
+ const jwk = keyset.current;
57
+ const source = keyset.source;
58
+ if (typeof jwk.d !== "string" || jwk.d.length === 0) {
59
+ throw new Error(`${source} current key must be a private JWK`);
32
60
  }
33
- if (!publishableKey) {
34
- publishableKey = process.env.SOFTWARE_PUBLISHABLE_KEY || process.env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY;
61
+ if (typeof jwk.kid !== "string" || jwk.kid.length === 0) {
62
+ throw new Error(`${source} must include kid`);
63
+ }
64
+ return jwk;
65
+ }
66
+ function signingKeyset() {
67
+ const raw = process.env[KEYSET_ENV];
68
+ const source = KEYSET_ENV;
69
+ if (raw) assertProductionKeysetUse(source);
70
+ const parsed = (() => {
71
+ if (!raw) return null;
72
+ try {
73
+ return JSON.parse(raw);
74
+ } catch {
75
+ throw new Error(`${KEYSET_ENV} is invalid JSON`);
76
+ }
77
+ })();
78
+ if (!parsed) throw new Error("MCP service JWT signing key is not configured");
79
+ const keys = Array.isArray(parsed.keys) ? parsed.keys : [parsed];
80
+ if (keys.length === 0 || keys.length > 2) {
81
+ throw new Error(
82
+ `${source} must contain one current key and at most one previous key`
83
+ );
84
+ }
85
+ const currentKid = parsed.current_kid;
86
+ if (typeof currentKid !== "string" && keys.length > 1) {
87
+ throw new Error(
88
+ `${source} must include current_kid when multiple keys are present`
89
+ );
90
+ }
91
+ const current = typeof currentKid === "string" ? keys.find((key) => key.kid === currentKid) : keys[0];
92
+ if (!current) throw new Error(`${source} current_kid is not in keys`);
93
+ return { current, keys, source };
94
+ }
95
+ function algForJwk(jwk) {
96
+ if (jwk.kty === "RSA") return "RS256";
97
+ if (jwk.kty === "EC" && jwk.crv === "P-256") return "ES256";
98
+ throw new Error("MCP service JWT signing key must be RSA or P-256 EC");
99
+ }
100
+ function toPublicJwk(jwk) {
101
+ const {
102
+ d: _d,
103
+ p: _p,
104
+ q: _q,
105
+ dp: _dp,
106
+ dq: _dq,
107
+ qi: _qi,
108
+ oth: _oth,
109
+ ...publicJwk
110
+ } = jwk;
111
+ return {
112
+ ...publicJwk,
113
+ alg: typeof publicJwk.alg === "string" ? publicJwk.alg : algForJwk(jwk),
114
+ use: "sig"
115
+ };
116
+ }
117
+ function base64urlJson(value) {
118
+ return Buffer.from(JSON.stringify(value)).toString("base64url");
119
+ }
120
+ function apiScopesFor(context) {
121
+ return context.scopes.includes("mcp:write") ? ["read", "write"] : ["read"];
122
+ }
123
+ function mcpServicePublicJwks() {
124
+ const keyset = signingKeyset();
125
+ const keys = /* @__PURE__ */ new Map();
126
+ for (const jwk of keyset.keys.map(toPublicJwk)) {
127
+ if (typeof jwk.kid === "string" && jwk.kid.length > 0) {
128
+ keys.set(jwk.kid, jwk);
129
+ }
130
+ }
131
+ return { keys: [...keys.values()] };
132
+ }
133
+ function signMcpServiceToken(context) {
134
+ if (!context.principalId) {
135
+ throw new Error("MCP OAuth principal is required for Console service auth");
136
+ }
137
+ const jwk = parsePrivateJwk();
138
+ const alg = algForJwk(jwk);
139
+ const now = Math.floor(Date.now() / 1e3);
140
+ const payload = {
141
+ iss: MCP_OAUTH_ISSUER,
142
+ aud: MCP_CONSOLE_SERVICE_AUDIENCE,
143
+ iat: now,
144
+ nbf: now,
145
+ exp: now + MCP_SERVICE_TOKEN_LIFETIME_SECONDS,
146
+ jti: randomUUID(),
147
+ sub: context.principalId,
148
+ act: {
149
+ sub: context.principalId,
150
+ tenant_id: context.tenantId
151
+ },
152
+ [MCP_TENANT_CLAIM]: context.tenantId,
153
+ [MCP_TENANT_ROLE_CLAIM]: context.tenantRole,
154
+ scope: MCP_CONSOLE_SERVICE_SCOPE,
155
+ api_scopes: apiScopesFor(context),
156
+ mcp_scopes: context.scopes
157
+ };
158
+ const header = { alg, kid: jwk.kid, typ: "JWT" };
159
+ const encodedHeader = base64urlJson(header);
160
+ const encodedPayload = base64urlJson(payload);
161
+ const signingInput = `${encodedHeader}.${encodedPayload}`;
162
+ const key = createPrivateKey({ key: jwk, format: "jwk" });
163
+ const signature = alg === "RS256" ? signBytes("RSA-SHA256", Buffer.from(signingInput), key) : signBytes("SHA256", Buffer.from(signingInput), {
164
+ key,
165
+ dsaEncoding: "ieee-p1363"
166
+ });
167
+ return `${signingInput}.${signature.toString("base64url")}`;
168
+ }
169
+
170
+ // src/lib/client.ts
171
+ var MISSING_HTTP_AUTH_CONTEXT_ERROR = "MCP HTTP requests require a validated OAuth tenant context before tool execution.";
172
+ function getClient() {
173
+ const oauthContext = tenantAuthContext();
174
+ if (oauthContext) {
175
+ const serviceToken = signMcpServiceToken(oauthContext);
176
+ const client = {
177
+ lastRequestId: null,
178
+ commerce: void 0,
179
+ collections: void 0,
180
+ community: void 0
181
+ };
182
+ const onRequestId = (id) => {
183
+ client.lastRequestId = id;
184
+ };
185
+ client.commerce = new ServerCommerceClient({
186
+ secretKey: serviceToken,
187
+ onRequestId
188
+ });
189
+ client.collections = new CollectionClient(
190
+ "",
191
+ serviceToken,
192
+ void 0,
193
+ void 0,
194
+ onRequestId
195
+ );
196
+ const community = new CommunityClient({ secretKey: serviceToken });
197
+ const moderation = new ModerationApi({ secretKey: serviceToken, onRequestId });
198
+ client.community = Object.assign(community, {
199
+ moderation: {
200
+ banCustomer: moderation.banCustomer.bind(moderation),
201
+ unbanCustomer: moderation.unbanCustomer.bind(moderation)
202
+ }
203
+ });
204
+ return client;
35
205
  }
206
+ if (hasRequestContext()) throw new Error(MISSING_HTTP_AUTH_CONTEXT_ERROR);
207
+ const secretKey = process.env.SOFTWARE_SECRET_KEY;
208
+ const publishableKey = process.env.SOFTWARE_PUBLISHABLE_KEY || process.env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY;
36
209
  if (!secretKey) {
37
210
  throw new Error(
38
- "Authentication required. Provide x-api-key header (HTTP) or SOFTWARE_SECRET_KEY env var (stdio)."
211
+ "Authentication required. Set SOFTWARE_SECRET_KEY for stdio transport."
39
212
  );
40
213
  }
41
214
  if (!secretKey.startsWith("sk01_") && !secretKey.startsWith("pat01_")) {
42
- throw new Error("Invalid API key format. Expected sk01_ or pat01_ token.");
215
+ throw new Error("Invalid SOFTWARE_SECRET_KEY format. Expected sk01_ or pat01_ token.");
43
216
  }
44
217
  if (!publishableKey) {
45
218
  throw new Error(
46
- "publishableKey is required. Provide X-Publishable-Key header (HTTP) or SOFTWARE_PUBLISHABLE_KEY env var (stdio). It is used for rate limiting and monthly quota enforcement via the edge proxy."
219
+ "publishableKey is required. Set SOFTWARE_PUBLISHABLE_KEY for stdio transport. It is used for rate limiting and monthly quota enforcement via the edge proxy."
47
220
  );
48
221
  }
49
222
  return createServerClient({
@@ -1087,49 +1260,40 @@ import { COLLECTIONS as COLLECTIONS8 } from "@01.software/sdk";
1087
1260
  import { createHash } from "crypto";
1088
1261
  var BASE_URL = process.env.SOFTWARE_API_URL || "http://localhost:3000";
1089
1262
  var TIMEOUT_MS = 5e3;
1263
+ var MISSING_HTTP_AUTH_CONTEXT_ERROR2 = "MCP HTTP requests require a validated OAuth tenant context before tool execution.";
1090
1264
  function resolveAuthHeaderContext() {
1091
- let context = {};
1092
- try {
1093
- const h = headers();
1094
- context = {
1095
- apiKey: h?.["x-api-key"],
1096
- publishableKey: h?.["x-publishable-key"] ?? h?.["x-client-key"]
1265
+ const oauthContext = tenantAuthContext();
1266
+ if (oauthContext) {
1267
+ return {
1268
+ apiKey: signMcpServiceToken(oauthContext),
1269
+ mode: "oauth"
1097
1270
  };
1098
- } catch {
1099
1271
  }
1272
+ if (hasRequestContext()) throw new Error(MISSING_HTTP_AUTH_CONTEXT_ERROR2);
1100
1273
  return {
1101
- apiKey: context.apiKey ?? process.env.SOFTWARE_SECRET_KEY,
1102
- publishableKey: context.publishableKey ?? process.env.SOFTWARE_PUBLISHABLE_KEY ?? process.env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY
1274
+ apiKey: process.env.SOFTWARE_SECRET_KEY,
1275
+ mode: "stdio",
1276
+ publishableKey: process.env.SOFTWARE_PUBLISHABLE_KEY ?? process.env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY
1103
1277
  };
1104
1278
  }
1105
1279
  function resolveApiKey() {
1106
1280
  const { apiKey } = resolveAuthHeaderContext();
1107
1281
  if (!apiKey || typeof apiKey !== "string") {
1108
1282
  throw new Error(
1109
- "Authentication required. Provide x-api-key header (HTTP) or SOFTWARE_SECRET_KEY env var (stdio)."
1283
+ "Authentication required. Set SOFTWARE_SECRET_KEY for stdio transport."
1110
1284
  );
1111
1285
  }
1112
1286
  return apiKey;
1113
1287
  }
1114
- function hashKey(apiKey) {
1115
- return createHash("sha256").update(apiKey).digest("hex");
1116
- }
1117
- function resolveAuthCacheKey(apiKey) {
1118
- const { publishableKey } = resolveAuthHeaderContext();
1119
- return hashKey(
1120
- JSON.stringify({
1121
- apiKey,
1122
- publishableKey: publishableKey ?? ""
1123
- })
1124
- );
1125
- }
1126
1288
  function buildAuthHeaders(apiKey) {
1127
- const { publishableKey } = resolveAuthHeaderContext();
1128
- const headers2 = {
1289
+ const { mode, publishableKey } = resolveAuthHeaderContext();
1290
+ const headers = {
1129
1291
  Authorization: `Bearer ${apiKey}`
1130
1292
  };
1131
- if (publishableKey) headers2["X-Publishable-Key"] = publishableKey;
1132
- return headers2;
1293
+ if (mode === "stdio" && publishableKey) {
1294
+ headers["X-Publishable-Key"] = publishableKey;
1295
+ }
1296
+ return headers;
1133
1297
  }
1134
1298
  function extractErrorMessage(body) {
1135
1299
  if (!body || typeof body !== "object") return void 0;
@@ -1223,37 +1387,18 @@ async function getCollectionSchemaTool({
1223
1387
  import { z as z28 } from "zod";
1224
1388
 
1225
1389
  // src/lib/tenant-context.ts
1226
- var TENANT_CONTEXT_CACHE_TTL_MS = 6e4;
1227
- var cache = /* @__PURE__ */ new Map();
1228
1390
  function getTenantContextPath(includeCounts) {
1229
1391
  return includeCounts ? "/api/tenants/context?counts=true" : "/api/tenants/context";
1230
1392
  }
1231
- function getCachedTenantContext(cacheKey) {
1232
- const cached = cache.get(cacheKey);
1233
- if (!cached || cached.expiry <= Date.now()) return void 0;
1234
- return cached.data;
1235
- }
1236
1393
  async function getTenantContext(includeCounts = false) {
1237
1394
  const apiKey = resolveApiKey();
1238
- const cacheKey = resolveAuthCacheKey(apiKey);
1239
- if (!includeCounts) {
1240
- const cached = getCachedTenantContext(cacheKey);
1241
- if (cached) return cached;
1242
- }
1243
1395
  const data = await consoleGet(
1244
1396
  getTenantContextPath(includeCounts),
1245
1397
  apiKey
1246
1398
  );
1247
- if (!includeCounts) {
1248
- cache.set(cacheKey, {
1249
- data,
1250
- expiry: Date.now() + TENANT_CONTEXT_CACHE_TTL_MS
1251
- });
1252
- }
1253
1399
  return data;
1254
1400
  }
1255
1401
  function invalidateTenantContextCache() {
1256
- cache.clear();
1257
1402
  }
1258
1403
 
1259
1404
  // src/tools/get-tenant-context.ts
@@ -1339,18 +1484,12 @@ async function handler({ includeCounts }) {
1339
1484
  import { z as z29 } from "zod";
1340
1485
 
1341
1486
  // src/lib/field-config.ts
1342
- var cache2 = /* @__PURE__ */ new Map();
1343
- var CACHE_TTL = 6e4;
1344
1487
  async function fetchFieldConfigs() {
1345
1488
  const apiKey = resolveApiKey();
1346
- const cacheKey = resolveAuthCacheKey(apiKey);
1347
- const cached = cache2.get(cacheKey);
1348
- if (cached && cached.expiry > Date.now()) return cached.data;
1349
1489
  const data = await consoleGet(
1350
1490
  "/api/field-configs/list",
1351
1491
  apiKey
1352
1492
  );
1353
- cache2.set(cacheKey, { data, expiry: Date.now() + CACHE_TTL });
1354
1493
  return data;
1355
1494
  }
1356
1495
  async function saveFieldConfig(body) {
@@ -1362,7 +1501,6 @@ async function saveFieldConfig(body) {
1362
1501
  );
1363
1502
  }
1364
1503
  function invalidateFieldConfigCache() {
1365
- cache2.clear();
1366
1504
  }
1367
1505
 
1368
1506
  // src/tools/list-configurable-fields.ts
@@ -1760,18 +1898,13 @@ const client = createClient({
1760
1898
  })
1761
1899
 
1762
1900
  // --- Register ---
1763
- const { customer, verificationRequired } = await client.customer.register({
1901
+ const { customer } = await client.customer.register({
1764
1902
  name: 'Jane Doe',
1765
1903
  email: 'jane@example.com',
1766
1904
  password: 'securePassword123',
1767
1905
  phone: '+821012345678', // optional
1768
1906
  })
1769
1907
 
1770
- if (verificationRequired) {
1771
- // Tenant has requireEmailVerification enabled.
1772
- // Token delivered via webhook \u2014 prompt user to check email.
1773
- }
1774
-
1775
1908
  // --- Login ---
1776
1909
  const { token, customer: loggedIn } = await client.customer.login({
1777
1910
  email: 'jane@example.com',
@@ -1792,9 +1925,9 @@ await client.customer.forgotPassword('jane@example.com') // sends token via webh
1792
1925
  await client.customer.resetPassword(token, 'newPassword123')`,
1793
1926
  cautions: [
1794
1927
  "customer.register/login/me are only available on Client (not ServerClient)",
1795
- "verificationRequired means no token is returned \u2014 user must verify email first",
1928
+ "registration creates a local customer account; add app-level verification if your project requires it",
1796
1929
  "updateProfile only accepts name, phone, and marketingConsent \u2014 not email or password",
1797
- "forgotPassword sends the token via tenant webhook, not directly to the client"
1930
+ "forgotPassword sends the token to configured tenant webhooks; your webhook handler owns email/SMS delivery"
1798
1931
  ],
1799
1932
  relatedResources: ["docs://sdk/customer-auth", "docs://sdk/getting-started"],
1800
1933
  relatedTools: []
@@ -2040,8 +2173,8 @@ var docIndex = [
2040
2173
  // Customer Auth
2041
2174
  {
2042
2175
  title: "Customer Auth \u2014 Login and Register",
2043
- keywords: ["customer", "login", "register", "auth", "authentication", "customer auth", "email verification", "verificationRequired"],
2044
- summary: "client.customer.login({ email, password }) and register({ name, email, password }). If tenant requireEmailVerification is on, register returns verificationRequired: true.",
2176
+ keywords: ["customer", "login", "register", "auth", "authentication", "customer auth"],
2177
+ summary: "client.customer.login({ email, password }) and register({ name, email, password }).",
2045
2178
  resourceUri: "docs://sdk/customer-auth"
2046
2179
  },
2047
2180
  {
@@ -2067,7 +2200,7 @@ var docIndex = [
2067
2200
  {
2068
2201
  title: "Webhooks",
2069
2202
  keywords: ["webhook", "hmac", "signature", "WEBHOOK_SECRET", "server-to-server", "event"],
2070
- summary: "Tenant webhooks deliver server-to-server events (e.g. email verification token, password reset token). Signed with HMAC-SHA256 using PAYLOAD_SECRET.",
2203
+ summary: "Tenant webhooks deliver server-to-server events such as password reset tokens. Signed with HMAC-SHA256 using PAYLOAD_SECRET.",
2071
2204
  resourceUri: "docs://sdk/webhook"
2072
2205
  },
2073
2206
  // Order API
@@ -2159,13 +2292,13 @@ var schema33 = {
2159
2292
  "server-client",
2160
2293
  "customer-auth",
2161
2294
  "mcp-connection",
2162
- "api-key-generation",
2295
+ "server-credentials",
2163
2296
  "webhook-verification"
2164
2297
  ]).describe("Authentication scenario")
2165
2298
  };
2166
2299
  var metadata33 = {
2167
2300
  name: "sdk-get-auth-setup",
2168
- description: "Get the correct authentication setup for a specific scenario. Returns env var names, code snippets, and security notes.",
2301
+ description: "Get the current authentication setup for a specific scenario. Returns env var names, code snippets, and security notes.",
2169
2302
  annotations: {
2170
2303
  title: "Get Auth Setup",
2171
2304
  readOnlyHint: true,
@@ -2198,15 +2331,14 @@ const { data } = client.query.useQuery({ collection: 'products' })`,
2198
2331
 
2199
2332
  const client = createServerClient({
2200
2333
  publishableKey: process.env.SOFTWARE_PUBLISHABLE_KEY!,
2201
- secretKey: process.env.SOFTWARE_SECRET_KEY! // usually sk01_..., sometimes pat01_...
2334
+ secretKey: process.env.SOFTWARE_SECRET_KEY!
2202
2335
  })
2203
2336
 
2204
2337
  // Full CRUD operations
2205
2338
  const result = await client.collections.from('products').create({ title: 'New Product' })`,
2206
2339
  notes: [
2207
- "ServerClient has full CRUD access \u2014 never expose the API key in browser",
2208
- "SOFTWARE_SECRET_KEY stores an opaque bearer token; backend services should prefer tenant API keys (sk01_...)",
2209
- "Browser-based CLI/init login flows may provision a user-scoped PAT (pat01_...) with a default tenant",
2340
+ "ServerClient has full CRUD access and must run only in trusted server code",
2341
+ "Store server credentials in environment variables and rotate them from the Console",
2210
2342
  "Use in API routes, server actions, or backend services only",
2211
2343
  "React Query hooks available for reads (useQuery, prefetchQuery, etc.) + mutations (useCreate, useUpdate, useRemove)"
2212
2344
  ]
@@ -2238,90 +2370,68 @@ client.customer.isAuthenticated()`,
2238
2370
  notes: [
2239
2371
  "Customer auth uses the browser Client (not ServerClient)",
2240
2372
  "JWT tokens are managed automatically by the SDK",
2241
- "Tenant may require email verification (requireEmailVerification setting)"
2373
+ "Registration creates a local customer account; add application-level verification if needed"
2242
2374
  ]
2243
2375
  },
2244
2376
  "mcp-connection": {
2245
2377
  title: "MCP Server Connection",
2246
- envVars: ["SOFTWARE_PUBLISHABLE_KEY", "SOFTWARE_SECRET_KEY"],
2378
+ envVars: [],
2247
2379
  code: `# Claude Code
2248
- claude mcp add --transport http \\
2249
- --header "x-api-key: $SOFTWARE_SECRET_KEY" \\
2250
- --header "x-publishable-key: $SOFTWARE_PUBLISHABLE_KEY" \\
2251
- 01software https://mcp.01.software/mcp
2380
+ claude mcp add --transport http 01software https://mcp.01.software/mcp
2252
2381
 
2253
- # Codex project-safe .codex/config.toml
2382
+ # Codex .codex/config.toml
2254
2383
  [mcp_servers.01software]
2255
2384
  url = "https://mcp.01.software/mcp"
2256
2385
 
2257
- [mcp_servers.01software.env_http_headers]
2258
- x-api-key = "SOFTWARE_SECRET_KEY"
2259
- x-publishable-key = "SOFTWARE_PUBLISHABLE_KEY"
2260
-
2261
- # Or use .mcp.json
2386
+ # Or use JSON clients that support OAuth discovery
2262
2387
  {
2263
2388
  "mcpServers": {
2264
2389
  "01software": {
2265
2390
  "type": "http",
2266
- "url": "https://mcp.01.software/mcp",
2267
- "headers": {
2268
- "x-api-key": "\${env:SOFTWARE_SECRET_KEY}",
2269
- "x-publishable-key": "\${env:SOFTWARE_PUBLISHABLE_KEY}"
2270
- }
2391
+ "url": "https://mcp.01.software/mcp"
2271
2392
  }
2272
2393
  }
2273
2394
  }`,
2274
2395
  notes: [
2275
- "MCP accepts either a tenant API key (sk01_...) or a personal access token (pat01_...) in x-api-key",
2276
- "HTTP transport also requires x-publishable-key (or legacy x-client-key) for tenant routing, rate limits, and quota enforcement",
2277
- "Codex project scope is appropriate for repo-safe shared configuration, but keep raw sk01_/pat01_ values out of committed files",
2278
- "Use tenant API keys for shared service integrations; PATs are useful for user-scoped local workflows",
2279
- "Never commit raw bearer tokens to repo-local MCP config; prefer environment interpolation, client prompts, OS secret managers, or ignored local files",
2280
- "Avoid passing real tokens directly on shared-machine command lines because shell history and process listings can expose them",
2281
- "stdio transport: use `npx @01.software/cli mcp` with SOFTWARE_PUBLISHABLE_KEY and SOFTWARE_SECRET_KEY env vars"
2396
+ "HTTP MCP uses OAuth discovery and Authorization Code + PKCE",
2397
+ "Clients that cannot complete OAuth discovery are unsupported until a smoke test proves compatibility",
2398
+ "stdio transport remains a local CLI path and is separate from HTTP MCP OAuth discovery"
2282
2399
  ]
2283
2400
  },
2284
- "api-key-generation": {
2285
- title: "API Key Generation",
2401
+ "server-credentials": {
2402
+ title: "Server Credential Management",
2286
2403
  envVars: ["SOFTWARE_PUBLISHABLE_KEY", "SOFTWARE_SECRET_KEY"],
2287
- code: `# API keys are generated from the Console, not in code.
2288
- # Go to Console > Settings > API Keys and click "Create API Key".
2289
- # The generated key has the format: sk01_{40hex}
2290
- # Copy the publishable key from the same tenant.
2404
+ code: `# Server credentials are managed from the Console, not in code.
2405
+ # Copy both values from the same tenant.
2291
2406
 
2292
- # Use them together for MCP, CLI, and server SDK calls:
2293
- export SOFTWARE_PUBLISHABLE_KEY=pk01_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
2407
+ # Use them together for CLI and server SDK calls.
2408
+ export SOFTWARE_PUBLISHABLE_KEY=pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
2294
2409
  export SOFTWARE_SECRET_KEY=sk01_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`,
2295
2410
  notes: [
2296
- "API keys are sk01_{40hex} opaque bearer tokens",
2297
2411
  "The matching SOFTWARE_PUBLISHABLE_KEY is still required for tenant routing, rate limits, and quota enforcement",
2298
- "Browser-based CLI/init login may issue pat01_{40hex} personal access tokens for user-scoped workflows",
2299
- "Used for MCP and REST API authentication via x-api-key header or Authorization: Bearer",
2300
- "Generate keys from Console > Settings > API Keys \u2014 never derive them in code"
2412
+ "Used for REST/SDK authentication in trusted server contexts",
2413
+ "Manage credentials from the Console and rotate them on exposure"
2301
2414
  ]
2302
2415
  },
2303
2416
  "webhook-verification": {
2304
2417
  title: "Webhook Verification",
2305
2418
  envVars: ["WEBHOOK_SECRET"],
2306
- code: `import { handleWebhook } from '@01.software/sdk/webhook'
2419
+ code: `import { handleWebhook, createCustomerAuthWebhookHandler } from '@01.software/sdk/webhook'
2420
+
2421
+ const customerAuthHandler = createCustomerAuthWebhookHandler({
2422
+ passwordReset: sendPasswordResetEmail,
2423
+ })
2307
2424
 
2308
2425
  export async function POST(request: Request) {
2309
- return handleWebhook(request, async (event) => {
2310
- // event.collection, event.operation, event.data
2311
- switch (event.operation) {
2312
- case 'verification':
2313
- await sendVerificationEmail(event.data)
2314
- break
2315
- case 'password-reset':
2316
- await sendPasswordResetEmail(event.data)
2317
- break
2318
- }
2319
- }, { secret: process.env.WEBHOOK_SECRET! })
2426
+ return handleWebhook(request, customerAuthHandler, {
2427
+ secret: process.env.WEBHOOK_SECRET!,
2428
+ })
2320
2429
  }`,
2321
2430
  notes: [
2322
2431
  "handleWebhook() takes (request, handler, options) \u2014 handler receives the parsed event",
2323
2432
  "WEBHOOK_SECRET is set per-tenant in Console > Settings",
2324
- "handleWebhook() verifies HMAC-SHA256 signature automatically before calling handler"
2433
+ "handleWebhook() verifies HMAC-SHA256 signature automatically before calling handler",
2434
+ "createCustomerAuthWebhookHandler() is optional; it just routes auth events to your own email/SMS delivery code"
2325
2435
  ]
2326
2436
  }
2327
2437
  };
@@ -2632,8 +2742,8 @@ await client.collections.from('products').remove('id')
2632
2742
  const { totalDocs } = await client.collections.from('products').count()
2633
2743
 
2634
2744
  // Metadata - generate Next.js Metadata from collection fields
2635
- // Auto-maps per-collection fields (e.g. posts: description\u2192description, thumbnail\u2192image)
2636
- const postMeta = await client.collections.from('posts').findMetadataById(id, { siteName: 'My Blog' })
2745
+ // Auto-maps per-collection fields (e.g. articles: description\u2192description, thumbnail\u2192image)
2746
+ const articleMeta = await client.collections.from('articles').findMetadataById(id, { siteName: 'My Blog' })
2637
2747
  const productMeta = await client.collections.from('products').findMetadata(
2638
2748
  { where: { slug: { equals: 'my-product' } } },
2639
2749
  { siteName: 'My Store' }
@@ -2959,7 +3069,7 @@ var schema38 = {
2959
3069
  feature: z38.enum([
2960
3070
  "ecommerce",
2961
3071
  "customers",
2962
- "posts",
3072
+ "articles",
2963
3073
  "documents",
2964
3074
  "playlists",
2965
3075
  "galleries",
@@ -3022,22 +3132,22 @@ customer-groups \u2014 Use \`create-collection\` with \`collection='customer-gro
3022
3132
 
3023
3133
  ### Config
3024
3134
 
3025
- - \`requireEmailVerification\` can be toggled in tenant settings
3135
+ - Customer registration creates a local account; add app-level verification if needed
3026
3136
  - Customer auth uses custom JWT (separate from Payload auth)`,
3027
- posts: `## Posts Setup Guide
3137
+ articles: `## Articles Setup Guide
3028
3138
 
3029
3139
  ### Required Collections (count > 0)
3030
3140
 
3031
- 1. **posts** \u2014 At least 1 post
3141
+ 1. **articles** \u2014 At least 1 article
3032
3142
  - Minimum fields: \`{ title, slug }\`
3033
3143
 
3034
- 2. **post-authors** \u2014 At least 1 author
3144
+ 2. **article-authors** \u2014 At least 1 author
3035
3145
  - Minimum fields: \`{ title, slug }\`
3036
- - Link authors to posts via the \`authors\` relationship field
3146
+ - Link authors to articles via the \`authors\` relationship field
3037
3147
 
3038
3148
  ### Optional Collections
3039
3149
 
3040
- post-categories, post-tags`,
3150
+ article-categories, article-tags`,
3041
3151
  documents: `## Documents Setup Guide
3042
3152
 
3043
3153
  ### Required Collections (count > 0)
@@ -3147,7 +3257,7 @@ form-submissions \u2014 Auto-created when forms are submitted by end users`,
3147
3257
 
3148
3258
  ### Required Collections (count > 0)
3149
3259
 
3150
- 1. **threads** \u2014 At least 1 thread
3260
+ 1. **posts** \u2014 At least 1 post
3151
3261
  - Minimum fields: \`{ title, slug }\`
3152
3262
 
3153
3263
  2. **reaction-types** \u2014 At least 1 reaction type defined
@@ -3159,7 +3269,7 @@ comments, reactions, bookmarks, reports, community-bans
3159
3269
 
3160
3270
  ### Optional Collections
3161
3271
 
3162
- thread-categories`
3272
+ post-categories`
3163
3273
  };
3164
3274
  function featureSetupGuide({ feature }) {
3165
3275
  return `# Feature Setup Guide: ${feature}
@@ -3188,23 +3298,11 @@ function handler6() {
3188
3298
 
3189
3299
  ## Authentication
3190
3300
 
3191
- All HTTP requests require a bearer token and publishable key:
3192
-
3193
- \`\`\`
3194
- x-api-key: <sk01_... or pat01_...>
3195
- x-publishable-key: <pk01_...>
3196
- \`\`\`
3197
-
3198
- \`x-client-key\` is accepted as a legacy alias for \`x-publishable-key\`.
3301
+ HTTP MCP uses OAuth discovery and Authorization Code + PKCE.
3199
3302
 
3200
- ### Accepted Token Types
3201
-
3202
- - \`sk01_{40hex}\` \u2014 tenant API key from Console > Settings > API Keys
3203
- - \`pat01_{40hex}\` \u2014 personal access token for user-scoped local workflows
3204
-
3205
- \`\`\`
3206
- SOFTWARE_SECRET_KEY=sk01_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
3207
- SOFTWARE_PUBLISHABLE_KEY=pk01_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
3303
+ \`\`\`toml
3304
+ [mcp_servers.01software]
3305
+ url = "https://mcp.01.software/mcp"
3208
3306
  \`\`\`
3209
3307
 
3210
3308
  ## Available Tools (34)
@@ -3308,7 +3406,17 @@ function handler7() {
3308
3406
  Carts: ["carts", "cart-items"],
3309
3407
  Discounts: ["discounts"],
3310
3408
  Documents: ["documents", "document-categories", "document-types"],
3311
- "Posts (Blog)": ["posts", "post-categories", "post-tags"],
3409
+ Articles: ["articles", "article-authors", "article-categories", "article-tags"],
3410
+ Community: [
3411
+ "posts",
3412
+ "comments",
3413
+ "reactions",
3414
+ "reaction-types",
3415
+ "bookmarks",
3416
+ "post-categories",
3417
+ "reports",
3418
+ "community-bans"
3419
+ ],
3312
3420
  Playlists: [
3313
3421
  "playlists",
3314
3422
  "tracks",
@@ -3859,7 +3967,7 @@ Customer authentication and profile management. Available on \`Client\` only (\`
3859
3967
  ### Authentication
3860
3968
  \`\`\`typescript
3861
3969
  // Register
3862
- const { customer, verificationRequired? } = await client.customer.register({
3970
+ const { customer } = await client.customer.register({
3863
3971
  name: 'John',
3864
3972
  email: 'john@example.com',
3865
3973
  password: 'password123',
@@ -3894,7 +4002,7 @@ const updated = await client.customer.updateProfile({
3894
4002
 
3895
4003
  ### Password
3896
4004
  \`\`\`typescript
3897
- // Forgot password (sends reset token via webhook)
4005
+ // Forgot password (sends reset token to configured tenant webhooks)
3898
4006
  await client.customer.forgotPassword(email)
3899
4007
 
3900
4008
  // Reset password with token
@@ -3904,11 +4012,6 @@ await client.customer.resetPassword(token, newPassword)
3904
4012
  await client.customer.changePassword(currentPassword, newPassword)
3905
4013
  \`\`\`
3906
4014
 
3907
- ### Email Verification
3908
- \`\`\`typescript
3909
- await client.customer.verifyEmail(token)
3910
- \`\`\`
3911
-
3912
4015
  ### Orders
3913
4016
  \`\`\`typescript
3914
4017
  const orders = await client.commerce.orders.listMine({
@@ -4087,7 +4190,7 @@ if (page1.hasNextPage) {
4087
4190
 
4088
4191
  \`\`\`typescript
4089
4192
  // Descending (newest first)
4090
- const result = await client.collections.from('posts').find({ sort: '-createdAt' })
4193
+ const result = await client.collections.from('articles').find({ sort: '-createdAt' })
4091
4194
 
4092
4195
  // Ascending
4093
4196
  const result2 = await client.collections.from('products').find({ sort: 'price' })
@@ -4382,19 +4485,19 @@ function handler13() {
4382
4485
 
4383
4486
  Server-side operations are available via \`client.commerce\` on \`ServerClient\`. Use \`createServerClient\` with both \`publishableKey\` and \`secretKey\`.
4384
4487
 
4385
- For backend services, prefer a tenant API key (\`sk01_...\`) in \`SOFTWARE_SECRET_KEY\`.
4386
- Browser-based CLI/init login flows may instead provision a user-scoped PAT (\`pat01_...\`) with a default tenant.
4387
-
4388
4488
  \`\`\`typescript
4389
4489
  import { createServerClient } from '@01.software/sdk'
4390
4490
 
4391
4491
  const client = createServerClient({
4392
4492
  publishableKey: process.env.SOFTWARE_PUBLISHABLE_KEY!,
4393
- secretKey: process.env.SOFTWARE_SECRET_KEY!, // usually sk01_..., sometimes pat01_...
4493
+ secretKey: process.env.SOFTWARE_SECRET_KEY!,
4394
4494
  })
4395
4495
  \`\`\`
4396
4496
 
4397
- > Never expose \`SOFTWARE_SECRET_KEY\` in browser code. Use server components, API routes, or server actions only.
4497
+ Use server components, API routes, or server actions only. Never expose
4498
+ \`SOFTWARE_SECRET_KEY\` to browser code, client bundles, logs, or public
4499
+ repositories. If a secret key leaks, rotate it from the Console before deploying
4500
+ again.
4398
4501
 
4399
4502
  ## Order API
4400
4503
 
@@ -4663,11 +4766,9 @@ const result = await client.customer.register({
4663
4766
  phone?: '+821012345678',
4664
4767
  })
4665
4768
  // result.customer - created customer object
4666
- // result.token? - JWT token (set if email verification not required)
4667
- // result.verificationRequired? - true if tenant requires email verification
4668
4769
  \`\`\`
4669
4770
 
4670
- When \`verificationRequired\` is true, no token is returned. The tenant's webhook receives a \`verificationToken\` to send to the customer.
4771
+ Registration creates a local customer account. Projects that need additional email verification should enforce it in application code.
4671
4772
 
4672
4773
  ### login()
4673
4774
  Authenticate with email and password.
@@ -4721,12 +4822,12 @@ const updated = await client.customer.updateProfile({
4721
4822
  ## Password
4722
4823
 
4723
4824
  ### forgotPassword()
4724
- Request a password reset. Sends reset token via tenant webhook.
4825
+ Request a password reset. Sends the reset token to configured tenant webhooks; your webhook handler owns delivery.
4725
4826
 
4726
4827
  \`\`\`typescript
4727
4828
  await client.customer.forgotPassword('john@example.com')
4728
4829
  // Rate limited: 5 requests/min per tenant+email
4729
- // Webhook receives: { resetPasswordToken, resetPasswordExpiry }
4830
+ // Webhook receives: { resetPasswordToken, resetPasswordExpiresAt }
4730
4831
  \`\`\`
4731
4832
 
4732
4833
  ### resetPassword()
@@ -4743,15 +4844,6 @@ Change password while authenticated (requires current password).
4743
4844
  await client.customer.changePassword('currentPassword', 'newPassword123')
4744
4845
  \`\`\`
4745
4846
 
4746
- ## Email Verification
4747
-
4748
- ### verifyEmail()
4749
- Verify email address using the token received via webhook.
4750
-
4751
- \`\`\`typescript
4752
- await client.customer.verifyEmail('verification-token')
4753
- \`\`\`
4754
-
4755
4847
  ## Orders
4756
4848
 
4757
4849
  ### listMine()
@@ -4797,12 +4889,7 @@ const client = createClient({
4797
4889
  async function handleRegister(email: string, password: string, name: string) {
4798
4890
  const result = await client.customer.register({ email, password, name })
4799
4891
 
4800
- if (result.verificationRequired) {
4801
- // Redirect to "check your email" page
4802
- return { status: 'verify-email' }
4803
- }
4804
-
4805
- // Token is automatically stored; customer is now logged in
4892
+ // Customer is created as a local account.
4806
4893
  return { status: 'success', customer: result.customer }
4807
4894
  }
4808
4895
 
@@ -4904,7 +4991,11 @@ await client.commerce.orders.checkout({ ... })
4904
4991
 
4905
4992
  **Environment variables**:
4906
4993
  - \`SOFTWARE_PUBLISHABLE_KEY\` \u2014 publishable key (no NEXT_PUBLIC prefix, server-only)
4907
- - \`SOFTWARE_SECRET_KEY\` \u2014 opaque bearer token (server-only, never expose to browser). Backend services usually use \`sk01_...\`; browser-based CLI/init login can provision \`pat01_...\` with a default tenant.
4994
+ - \`SOFTWARE_SECRET_KEY\` \u2014 server credential
4995
+
4996
+ Never expose \`SOFTWARE_SECRET_KEY\` in browser code, client bundles, logs, or
4997
+ public repositories. If a secret key leaks, rotate it from the Console before
4998
+ deploying again.
4908
4999
 
4909
5000
  ## Decision Matrix
4910
5001
 
@@ -4970,9 +5061,10 @@ export function ProductList() {
4970
5061
 
4971
5062
  ## Security Rules
4972
5063
 
4973
- - Never import \`SOFTWARE_SECRET_KEY\` or \`SOFTWARE_PUBLISHABLE_KEY\` in files without a \`'use server'\` directive or that could be bundled for the browser.
5064
+ - Keep server credentials in server-only modules.
4974
5065
  - Only \`NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY\` is safe to use in client components.
4975
- - If you accidentally expose \`SOFTWARE_SECRET_KEY\` in a browser bundle, rotate the key immediately in the 01.software console.
5066
+ - Never import a module that reads \`SOFTWARE_SECRET_KEY\` from a client component.
5067
+ - Rotate any exposed secret key immediately from the Console.
4976
5068
 
4977
5069
  > Ecommerce note: product card pricing lives on \`products.listing.*\`, but authoritative sellable pricing still lives on \`product-variants.price\`.`;
4978
5070
  }
@@ -5137,27 +5229,23 @@ var metadata50 = {
5137
5229
  function handler17() {
5138
5230
  return `# Webhooks
5139
5231
 
5140
- The platform dispatches HMAC-SHA256 signed webhook events to your registered URLs for async operations (email verification, password reset, etc.).
5232
+ The platform dispatches HMAC-SHA256 signed webhook events to your registered URLs. Tenant developers own routing inside their webhook handler.
5141
5233
 
5142
5234
  ## Webhook Handling
5143
5235
 
5144
- Use the SDK \`handleWebhook\` helper to verify signatures and route events.
5236
+ Use the SDK \`handleWebhook\` helper to verify signatures. For customer auth events, use \`createCustomerAuthWebhookHandler\` to wire delivery behavior in your app.
5145
5237
 
5146
5238
  \`\`\`typescript
5147
- import { handleWebhook } from '@01.software/sdk/webhook'
5239
+ import { handleWebhook, createCustomerAuthWebhookHandler } from '@01.software/sdk/webhook'
5240
+
5241
+ const handler = createCustomerAuthWebhookHandler({
5242
+ passwordReset: async (data) => {
5243
+ await sendPasswordResetEmail(data)
5244
+ },
5245
+ })
5148
5246
 
5149
5247
  export async function POST(request: Request) {
5150
- return handleWebhook(request, async (event) => {
5151
- // event.collection, event.operation, event.data
5152
- switch (event.operation) {
5153
- case 'verification':
5154
- await sendVerificationEmail(event.data)
5155
- break
5156
- case 'password-reset':
5157
- await sendPasswordResetEmail(event.data)
5158
- break
5159
- }
5160
- }, {
5248
+ return handleWebhook(request, handler, {
5161
5249
  secret: process.env.WEBHOOK_SECRET!,
5162
5250
  })
5163
5251
  }
@@ -5169,19 +5257,17 @@ export async function POST(request: Request) {
5169
5257
 
5170
5258
  \`\`\`typescript
5171
5259
  // app/api/webhooks/route.ts
5172
- import { handleWebhook } from '@01.software/sdk/webhook'
5260
+ import { handleWebhook, createCustomerAuthWebhookHandler } from '@01.software/sdk/webhook'
5261
+
5262
+ const customerAuthHandler = createCustomerAuthWebhookHandler({
5263
+ passwordReset: sendPasswordResetEmail,
5264
+ })
5173
5265
 
5174
5266
  export async function POST(request: Request) {
5175
5267
  return handleWebhook(request, async (event) => {
5176
5268
  console.log('Webhook received:', event.collection, event.operation)
5177
5269
 
5178
- if (event.collection === 'customers') {
5179
- if (event.operation === 'verification') {
5180
- await sendVerificationEmail(event.data)
5181
- } else if (event.operation === 'password-reset') {
5182
- await sendPasswordResetEmail(event.data)
5183
- }
5184
- }
5270
+ await customerAuthHandler(event)
5185
5271
  }, {
5186
5272
  secret: process.env.WEBHOOK_SECRET!,
5187
5273
  })
@@ -5195,49 +5281,13 @@ All webhook events share this envelope:
5195
5281
  \`\`\`typescript
5196
5282
  {
5197
5283
  collection: string, // e.g. 'customers'
5198
- operation: string, // e.g. 'verification' | 'password-reset'
5284
+ operation: string, // e.g. 'password-reset'
5199
5285
  data: object, // event-specific payload
5200
5286
  }
5201
5287
  \`\`\`
5202
5288
 
5203
5289
  ## Event Types
5204
5290
 
5205
- ### Customer Email Verification
5206
-
5207
- Dispatched when a customer registers on a tenant with \`requireEmailVerification: true\`.
5208
-
5209
- \`\`\`typescript
5210
- {
5211
- collection: 'customers',
5212
- operation: 'verification',
5213
- data: {
5214
- customerId: string,
5215
- email: string,
5216
- name: string,
5217
- verificationToken: string, // raw token to include in verification link
5218
- }
5219
- }
5220
- \`\`\`
5221
-
5222
- **Usage**: Send the \`verificationToken\` to the customer's email. The customer calls \`client.customer.verifyEmail(token)\` to complete verification.
5223
-
5224
- \`\`\`typescript
5225
- // Example: send verification email
5226
- async function sendVerificationEmail(data: {
5227
- customerId: string
5228
- email: string
5229
- name: string
5230
- verificationToken: string
5231
- }) {
5232
- const verifyUrl = \`https://yourstore.com/verify-email?token=\${data.verificationToken}\`
5233
- await emailService.send({
5234
- to: data.email,
5235
- subject: 'Verify your email',
5236
- body: \`Click here to verify: \${verifyUrl}\`,
5237
- })
5238
- }
5239
- \`\`\`
5240
-
5241
5291
  ### Customer Password Reset
5242
5292
 
5243
5293
  Dispatched when a customer calls \`client.customer.forgotPassword(email)\`.
@@ -5251,7 +5301,7 @@ Dispatched when a customer calls \`client.customer.forgotPassword(email)\`.
5251
5301
  email: string,
5252
5302
  name: string,
5253
5303
  resetPasswordToken: string, // raw token to include in reset link
5254
- resetPasswordExpiry: string, // ISO 8601 expiry (1 hour from dispatch)
5304
+ resetPasswordExpiresAt: string, // ISO 8601 expiry (1 hour from dispatch)
5255
5305
  }
5256
5306
  }
5257
5307
  \`\`\`
@@ -5264,13 +5314,13 @@ async function sendPasswordResetEmail(data: {
5264
5314
  email: string
5265
5315
  name: string
5266
5316
  resetPasswordToken: string
5267
- resetPasswordExpiry: string
5317
+ resetPasswordExpiresAt: string
5268
5318
  }) {
5269
5319
  const resetUrl = \`https://yourstore.com/reset-password?token=\${data.resetPasswordToken}\`
5270
5320
  await emailService.send({
5271
5321
  to: data.email,
5272
5322
  subject: 'Reset your password',
5273
- body: \`Reset link (expires \${data.resetPasswordExpiry}): \${resetUrl}\`,
5323
+ body: \`Reset link (expires \${data.resetPasswordExpiresAt}): \${resetUrl}\`,
5274
5324
  })
5275
5325
  }
5276
5326
  \`\`\`
@@ -5333,37 +5383,40 @@ function registerStaticResource(server, uri, meta, handler19) {
5333
5383
  })
5334
5384
  );
5335
5385
  }
5336
- function createServer() {
5386
+ function createServer(options = {}) {
5387
+ const toolSurface = options.toolSurface ?? "full";
5337
5388
  const server = new McpServer({
5338
5389
  name: "01.software MCP Server",
5339
5390
  version: "0.1.0"
5340
5391
  });
5341
- registerTool(server, schema, metadata, queryCollection);
5342
- registerTool(server, schema2, metadata2, getCollectionById);
5343
- registerTool(server, schema3, metadata3, createCollection);
5344
- registerTool(server, schema4, metadata4, updateCollection);
5345
- registerTool(server, schema5, metadata5, deleteCollection);
5346
- registerTool(server, schema6, metadata6, deleteManyCollection);
5347
- registerTool(server, schema7, metadata7, updateManyCollection);
5348
- registerTool(server, schema8, metadata8, getOrder);
5349
- registerTool(server, schema9, metadata9, createOrder);
5350
- registerTool(server, schema10, metadata10, updateOrder);
5351
- registerTool(server, schema11, metadata11, checkout);
5352
- registerTool(server, schema12, metadata12, createFulfillment);
5353
- registerTool(server, schema13, metadata13, updateFulfillment);
5354
- registerTool(server, schema14, metadata14, updateTransaction);
5355
- registerTool(server, schema15, metadata15, createReturn);
5356
- registerTool(server, schema16, metadata16, updateReturn);
5357
- registerTool(server, schema17, metadata17, returnWithRefund);
5358
- registerTool(server, schema18, metadata18, addCartItem);
5359
- registerTool(server, schema19, metadata19, updateCartItem);
5360
- registerTool(server, schema20, metadata20, removeCartItem);
5361
- registerTool(server, schema21, metadata21, applyDiscount);
5362
- registerTool(server, schema22, metadata22, removeDiscount);
5363
- registerTool(server, schema23, metadata23, clearCart);
5364
- registerTool(server, schema24, metadata24, validateDiscount);
5365
- registerTool(server, schema25, metadata25, calculateShipping);
5366
- registerTool(server, schema26, metadata26, stockCheck);
5392
+ if (toolSurface === "full") {
5393
+ registerTool(server, schema, metadata, queryCollection);
5394
+ registerTool(server, schema2, metadata2, getCollectionById);
5395
+ registerTool(server, schema3, metadata3, createCollection);
5396
+ registerTool(server, schema4, metadata4, updateCollection);
5397
+ registerTool(server, schema5, metadata5, deleteCollection);
5398
+ registerTool(server, schema6, metadata6, deleteManyCollection);
5399
+ registerTool(server, schema7, metadata7, updateManyCollection);
5400
+ registerTool(server, schema8, metadata8, getOrder);
5401
+ registerTool(server, schema9, metadata9, createOrder);
5402
+ registerTool(server, schema10, metadata10, updateOrder);
5403
+ registerTool(server, schema11, metadata11, checkout);
5404
+ registerTool(server, schema12, metadata12, createFulfillment);
5405
+ registerTool(server, schema13, metadata13, updateFulfillment);
5406
+ registerTool(server, schema14, metadata14, updateTransaction);
5407
+ registerTool(server, schema15, metadata15, createReturn);
5408
+ registerTool(server, schema16, metadata16, updateReturn);
5409
+ registerTool(server, schema17, metadata17, returnWithRefund);
5410
+ registerTool(server, schema18, metadata18, addCartItem);
5411
+ registerTool(server, schema19, metadata19, updateCartItem);
5412
+ registerTool(server, schema20, metadata20, removeCartItem);
5413
+ registerTool(server, schema21, metadata21, applyDiscount);
5414
+ registerTool(server, schema22, metadata22, removeDiscount);
5415
+ registerTool(server, schema23, metadata23, clearCart);
5416
+ registerTool(server, schema24, metadata24, validateDiscount);
5417
+ registerTool(server, schema25, metadata25, calculateShipping);
5418
+ registerTool(server, schema26, metadata26, stockCheck);
5419
+ }
5367
5420
  registerTool(server, schema27, metadata27, getCollectionSchemaTool);
5368
5421
  registerTool(server, schema28, metadata28, handler);
5369
5422
  registerTool(server, schema29, metadata29, listConfigurableFields);
@@ -5392,21 +5445,198 @@ function createServer() {
5392
5445
  }
5393
5446
 
5394
5447
  // src/auth.ts
5395
- function validateApiKey(apiKey) {
5396
- if (!apiKey || apiKey.length === 0) {
5397
- return { valid: false, error: "x-api-key header is required" };
5448
+ import { createPublicKey, verify as verifySignature } from "crypto";
5449
+ import {
5450
+ MCP_OAUTH_ISSUER as MCP_OAUTH_ISSUER2,
5451
+ MCP_RESOURCE_AUDIENCE,
5452
+ MCP_SCOPES,
5453
+ MCP_TENANT_CLAIM as MCP_TENANT_CLAIM2,
5454
+ MCP_TENANT_ROLE_CLAIM as MCP_TENANT_ROLE_CLAIM2
5455
+ } from "@01.software/auth-contracts";
5456
+ var ALLOWED_ALGORITHMS = /* @__PURE__ */ new Set(["RS256", "ES256"]);
5457
+ var DEFAULT_CLOCK_SKEW_SECONDS = 30;
5458
+ var DEFAULT_JWKS_URI = `${MCP_OAUTH_ISSUER2}/.well-known/jwks.json`;
5459
+ var MAX_ACCESS_TOKEN_LIFETIME_SECONDS = 300;
5460
+ function invalid(errorDescription) {
5461
+ return { valid: false, error: "invalid_token", errorDescription };
5462
+ }
5463
+ function isProduction() {
5464
+ return process.env.NODE_ENV === "production";
5465
+ }
5466
+ function insufficientScope(errorDescription) {
5467
+ return { valid: false, error: "insufficient_scope", errorDescription };
5468
+ }
5469
+ function decodeBase64UrlJson(value) {
5470
+ try {
5471
+ return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
5472
+ } catch {
5473
+ return null;
5398
5474
  }
5399
- if (!apiKey.startsWith("sk01_") && !apiKey.startsWith("pat01_")) {
5400
- return {
5401
- valid: false,
5402
- error: "Invalid API key format. Expected sk01_ or pat01_ token."
5403
- };
5475
+ }
5476
+ function parseScopes(payload) {
5477
+ const rawScopes = typeof payload.scope === "string" ? payload.scope.split(/\s+/).filter(Boolean) : Array.isArray(payload.scp) ? payload.scp : [];
5478
+ return rawScopes.filter(
5479
+ (scope) => scope === MCP_SCOPES.read || scope === MCP_SCOPES.write
5480
+ );
5481
+ }
5482
+ function audienceMatches(aud, expected) {
5483
+ if (typeof aud === "string") return aud === expected;
5484
+ return Array.isArray(aud) && aud.includes(expected);
5485
+ }
5486
+ function verifyJwtSignature(alg, jwk, signingInput, signature) {
5487
+ const key = createPublicKey({ key: jwk, format: "jwk" });
5488
+ const input = Buffer.from(signingInput);
5489
+ if (alg === "RS256") {
5490
+ return verifySignature("RSA-SHA256", input, key, signature);
5491
+ }
5492
+ if (alg === "ES256") {
5493
+ return verifySignature("SHA256", input, { key, dsaEncoding: "ieee-p1363" }, signature);
5494
+ }
5495
+ return false;
5496
+ }
5497
+ var cachedRemoteJwks = null;
5498
+ function jwksUriFor(options) {
5499
+ const raw = isProduction() ? DEFAULT_JWKS_URI : options.jwksUri ?? DEFAULT_JWKS_URI;
5500
+ let url;
5501
+ try {
5502
+ url = new URL(raw);
5503
+ } catch {
5504
+ return null;
5505
+ }
5506
+ if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash) {
5507
+ return null;
5508
+ }
5509
+ return url.toString();
5510
+ }
5511
+ async function remoteJwks(options = {}, forceRefresh = false) {
5512
+ const uri = jwksUriFor(options);
5513
+ if (!uri) return null;
5514
+ const now = Date.now();
5515
+ if (!forceRefresh && cachedRemoteJwks && cachedRemoteJwks.uri === uri && cachedRemoteJwks.expiresAt > now) {
5516
+ return cachedRemoteJwks.jwks;
5517
+ }
5518
+ const fetchImpl = options.fetchImpl ?? fetch;
5519
+ let response;
5520
+ try {
5521
+ response = await fetchImpl(uri, {
5522
+ headers: { Accept: "application/json" }
5523
+ });
5524
+ } catch {
5525
+ return null;
5526
+ }
5527
+ if (!response.ok) return null;
5528
+ const parsed = await response.json().catch(() => null);
5529
+ if (!parsed || !Array.isArray(parsed.keys)) return null;
5530
+ cachedRemoteJwks = {
5531
+ expiresAt: now + 3e5,
5532
+ jwks: parsed,
5533
+ uri
5534
+ };
5535
+ return parsed;
5536
+ }
5537
+ function validateAccessToken(token, options = {}) {
5538
+ const parts = token.split(".");
5539
+ if (parts.length !== 3 || parts.some((part) => part.length === 0)) {
5540
+ return invalid("Bearer token must be a compact JWT");
5541
+ }
5542
+ const [encodedHeader, encodedPayload, encodedSignature] = parts;
5543
+ const header = decodeBase64UrlJson(encodedHeader);
5544
+ const payload = decodeBase64UrlJson(encodedPayload);
5545
+ if (!header || !payload) return invalid("Bearer token contains invalid JSON");
5546
+ if (typeof header.alg !== "string" || !ALLOWED_ALGORITHMS.has(header.alg)) {
5547
+ return invalid("Bearer token uses an unsupported signing algorithm");
5548
+ }
5549
+ if (typeof header.kid !== "string" || header.kid.length === 0) {
5550
+ return invalid("Bearer token is missing kid");
5551
+ }
5552
+ const jwks = options.jwks;
5553
+ if (!jwks) return invalid("JWKS is not configured");
5554
+ const jwk = jwks.keys.find((key) => key.kid === header.kid);
5555
+ if (!jwk) return invalid("Bearer token kid is unknown");
5556
+ const signingInput = `${encodedHeader}.${encodedPayload}`;
5557
+ const signature = Buffer.from(encodedSignature, "base64url");
5558
+ if (!verifyJwtSignature(header.alg, jwk, signingInput, signature)) {
5559
+ return invalid("Bearer token signature is invalid");
5560
+ }
5561
+ const issuer = options.issuer ?? MCP_OAUTH_ISSUER2;
5562
+ if (payload.iss !== issuer) return invalid("Bearer token issuer is invalid");
5563
+ const audience = options.audience ?? MCP_RESOURCE_AUDIENCE;
5564
+ if (!audienceMatches(payload.aud, audience)) {
5565
+ return invalid("Bearer token audience is invalid");
5566
+ }
5567
+ if (typeof payload.iat !== "number") return invalid("Bearer token is missing iat");
5568
+ if (typeof payload.exp !== "number") return invalid("Bearer token is missing exp");
5569
+ if (payload.exp <= payload.iat) {
5570
+ return invalid("Bearer token lifetime is invalid");
5571
+ }
5572
+ if (payload.exp - payload.iat > MAX_ACCESS_TOKEN_LIFETIME_SECONDS) {
5573
+ return invalid("Bearer token lifetime exceeds 300 seconds");
5574
+ }
5575
+ const nowSeconds = Math.floor((options.now ?? /* @__PURE__ */ new Date()).getTime() / 1e3);
5576
+ const leeway = options.clockSkewSeconds ?? DEFAULT_CLOCK_SKEW_SECONDS;
5577
+ if (payload.iat > nowSeconds + leeway) {
5578
+ return invalid("Bearer token iat is too far in the future");
5579
+ }
5580
+ if (typeof payload.nbf === "number" && payload.nbf > nowSeconds + leeway) {
5581
+ return invalid("Bearer token is not yet valid");
5582
+ }
5583
+ if (payload.exp < nowSeconds - leeway) return invalid("Bearer token is expired");
5584
+ const tenantId = payload[MCP_TENANT_CLAIM2];
5585
+ if (typeof tenantId !== "string" || tenantId.length === 0) {
5586
+ return invalid("Bearer token tenant_id claim is invalid");
5404
5587
  }
5405
- return { valid: true };
5588
+ const tenantRole = payload[MCP_TENANT_ROLE_CLAIM2];
5589
+ if (tenantRole !== "tenant-admin" && tenantRole !== "tenant-editor" && tenantRole !== "tenant-viewer") {
5590
+ return invalid("Bearer token tenant_role claim is invalid");
5591
+ }
5592
+ if (typeof payload.sub !== "string" || payload.sub.length === 0) {
5593
+ return invalid("Bearer token subject claim is invalid");
5594
+ }
5595
+ const scopes = parseScopes(payload);
5596
+ const requiredScopes = options.requiredScopes ?? [MCP_SCOPES.read];
5597
+ const missingScope = requiredScopes.find((scope) => !scopes.includes(scope));
5598
+ if (missingScope) return insufficientScope(`Bearer token is missing ${missingScope}`);
5599
+ return {
5600
+ valid: true,
5601
+ context: {
5602
+ tenantId,
5603
+ principalId: payload.sub,
5604
+ scopes,
5605
+ tenantRole,
5606
+ authMode: "oauth"
5607
+ }
5608
+ };
5609
+ }
5610
+ function shouldRefreshRemoteJwks(result) {
5611
+ return !result.valid && (result.errorDescription === "Bearer token kid is unknown" || result.errorDescription === "Bearer token signature is invalid");
5612
+ }
5613
+ async function validateBearerAuthorizationHeaderAsync(authorization, options) {
5614
+ if (!authorization) return invalid("Authorization Bearer token is required");
5615
+ const match = authorization.match(/^Bearer\s+(.+)$/i);
5616
+ if (!match) return invalid("Authorization header must use Bearer");
5617
+ if (options?.jwks) {
5618
+ return validateAccessToken(match[1], options);
5619
+ }
5620
+ const jwks = await remoteJwks(options);
5621
+ const result = validateAccessToken(match[1], {
5622
+ ...options,
5623
+ jwks: jwks ?? void 0
5624
+ });
5625
+ if (shouldRefreshRemoteJwks(result)) {
5626
+ const refreshedJwks = await remoteJwks(options, true);
5627
+ if (refreshedJwks) {
5628
+ return validateAccessToken(match[1], {
5629
+ ...options,
5630
+ jwks: refreshedJwks
5631
+ });
5632
+ }
5633
+ }
5634
+ return result;
5406
5635
  }
5407
5636
 
5408
5637
  // src/handler.ts
5409
5638
  var MAX_REQUEST_BODY_BYTES = 1024 * 1024;
5639
+ var MCP_ALLOWED_BROWSER_ORIGIN = "https://01.software";
5410
5640
  var METHOD_NOT_ALLOWED = JSON.stringify({
5411
5641
  jsonrpc: "2.0",
5412
5642
  error: {
@@ -5416,16 +5646,16 @@ var METHOD_NOT_ALLOWED = JSON.stringify({
5416
5646
  id: null
5417
5647
  });
5418
5648
  function setCors(res) {
5419
- res.setHeader("Access-Control-Allow-Origin", "*");
5649
+ res.setHeader("Access-Control-Allow-Origin", MCP_ALLOWED_BROWSER_ORIGIN);
5420
5650
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
5421
5651
  res.setHeader(
5422
5652
  "Access-Control-Allow-Headers",
5423
- "Content-Type, x-api-key, x-publishable-key, x-client-key, mcp-session-id"
5653
+ "Authorization, Content-Type, mcp-session-id"
5424
5654
  );
5425
5655
  res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id, Mcp-Protocol-Version");
5426
5656
  }
5427
- function getHeaderValue(headers2, name) {
5428
- const value = headers2[name.toLowerCase()];
5657
+ function getHeaderValue(headers, name) {
5658
+ const value = headers[name.toLowerCase()];
5429
5659
  if (Array.isArray(value)) return value[0];
5430
5660
  return value;
5431
5661
  }
@@ -5450,143 +5680,69 @@ var HOME_PAGE = `01.software MCP Server
5450
5680
  ======================
5451
5681
 
5452
5682
  MCP server for AI agents to interact with the 01.software API.
5453
- Manage content, products, orders, and more.
5683
+ Connect over HTTP with OAuth, or use the local CLI stdio transport for full
5684
+ server-key operations.
5454
5685
 
5455
5686
 
5456
5687
  Authentication
5457
5688
  --------------
5458
5689
 
5459
- All requests require both:
5460
- x-api-key opaque bearer token
5461
- x-publishable-key publishable key for routing, rate limits, and quota enforcement
5462
-
5463
- Use x-client-key as a legacy alias for x-publishable-key.
5464
-
5465
- Accepted formats:
5466
- sk01_{40hex} tenant API key (Console > Settings > API Keys)
5467
- pat01_{40hex} personal access token (user-scoped local workflow)
5468
- pk01_{...} publishable key
5469
-
5470
- export SOFTWARE_PUBLISHABLE_KEY=pk01_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
5471
- export SOFTWARE_SECRET_KEY=sk01_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
5472
-
5690
+ HTTP MCP uses OAuth discovery and Authorization Code + PKCE.
5691
+ Clients should connect to:
5473
5692
 
5474
5693
  Connect
5475
5694
  -------
5476
5695
 
5477
5696
  Claude Code:
5478
5697
 
5479
- claude mcp add --transport http \\
5480
- --header "x-api-key: $SOFTWARE_SECRET_KEY" \\
5481
- --header "x-publishable-key: $SOFTWARE_PUBLISHABLE_KEY" \\
5482
- 01software https://mcp.01.software/mcp
5698
+ claude mcp add --transport http 01software https://mcp.01.software/mcp
5483
5699
 
5484
- Codex (.codex/config.toml, project-safe when using env vars):
5700
+ Codex (.codex/config.toml):
5485
5701
 
5486
5702
  [mcp_servers.01software]
5487
5703
  url = "https://mcp.01.software/mcp"
5488
5704
 
5489
- [mcp_servers.01software.env_http_headers]
5490
- x-api-key = "SOFTWARE_SECRET_KEY"
5491
- x-publishable-key = "SOFTWARE_PUBLISHABLE_KEY"
5705
+ Cursor / Claude Desktop / other JSON clients:
5492
5706
 
5493
- // or .mcp.json
5494
5707
  {
5495
5708
  "mcpServers": {
5496
5709
  "01software": {
5497
5710
  "type": "http",
5498
- "url": "https://mcp.01.software/mcp",
5499
- "headers": {
5500
- "x-api-key": "\${env:SOFTWARE_SECRET_KEY}",
5501
- "x-publishable-key": "\${env:SOFTWARE_PUBLISHABLE_KEY}"
5502
- }
5711
+ "url": "https://mcp.01.software/mcp"
5503
5712
  }
5504
5713
  }
5505
5714
  }
5506
5715
 
5507
- Cursor (.cursor/mcp.json):
5716
+ Windsurf (~/.codeium/windsurf/mcp_config.json):
5508
5717
 
5509
5718
  {
5510
5719
  "mcpServers": {
5511
5720
  "01software": {
5512
- "url": "https://mcp.01.software/mcp",
5513
- "headers": {
5514
- "x-api-key": "\${env:SOFTWARE_SECRET_KEY}",
5515
- "x-publishable-key": "\${env:SOFTWARE_PUBLISHABLE_KEY}"
5516
- }
5721
+ "serverUrl": "https://mcp.01.software/mcp"
5517
5722
  }
5518
5723
  }
5519
5724
  }
5520
5725
 
5521
- Windsurf (~/.codeium/windsurf/mcp_config.json):
5726
+ CLI (stdio):
5522
5727
 
5523
- {
5524
- "mcpServers": {
5525
- "01software": {
5526
- "serverUrl": "https://mcp.01.software/mcp",
5527
- "headers": {
5528
- "x-api-key": "\${env:SOFTWARE_SECRET_KEY}",
5529
- "x-publishable-key": "\${env:SOFTWARE_PUBLISHABLE_KEY}"
5530
- }
5531
- }
5532
- }
5533
- }
5728
+ npx @01.software/cli mcp
5534
5729
 
5535
- VS Code (.vscode/mcp.json):
5536
5730
 
5537
- {
5538
- "servers": {
5539
- "01software": {
5540
- "type": "http",
5541
- "url": "https://mcp.01.software/mcp",
5542
- "headers": {
5543
- "x-api-key": "\${input:01software-api-key}",
5544
- "x-publishable-key": "\${input:01software-publishable-key}"
5545
- }
5546
- }
5547
- }
5548
- }
5731
+ HTTP OAuth Tools
5732
+ ----------------
5549
5733
 
5550
- Claude Desktop (claude_desktop_config.json):
5734
+ Schema get-collection-schema
5735
+ Context get-tenant-context
5736
+ Field Config list-configurable-fields, update-field-config
5737
+ Guidance sdk-get-recipe, sdk-search-docs, sdk-get-auth-setup, sdk-get-collection-pattern
5551
5738
 
5552
- {
5553
- "mcpServers": {
5554
- "01software": {
5555
- "url": "https://mcp.01.software/mcp",
5556
- "headers": {
5557
- "x-api-key": "\${env:SOFTWARE_SECRET_KEY}",
5558
- "x-publishable-key": "\${env:SOFTWARE_PUBLISHABLE_KEY}"
5559
- }
5560
- }
5561
- }
5562
- }
5739
+ Full Tool Surface
5740
+ -----------------
5563
5741
 
5564
- CLI (stdio):
5742
+ The local CLI stdio transport exposes CRUD, commerce, cart, validation, product,
5743
+ schema, context, field-config, and guidance tools:
5565
5744
 
5566
5745
  npx @01.software/cli mcp
5567
- # Reads SOFTWARE_SECRET_KEY=sk01_... or pat01_...
5568
- # Also reads SOFTWARE_PUBLISHABLE_KEY for CDN routing, rate limits, and quota enforcement
5569
-
5570
- Security: never commit raw sk01_... or pat01_... tokens to repo-local MCP
5571
- config files. Prefer client secret prompts, environment interpolation, OS secret
5572
- managers, or ignored local files. Avoid passing real tokens directly on
5573
- shared-machine command lines because shell history and process listings can
5574
- expose them.
5575
-
5576
-
5577
- Tools (34)
5578
- ----------
5579
-
5580
- CRUD query, get, create, update, delete, delete-many, update-many
5581
- Orders create-order, checkout, get-order, update-order, create-fulfillment, update-fulfillment, update-transaction
5582
- Returns create-return, update-return, return-with-refund
5583
- Cart add-cart-item, update-cart-item, remove-cart-item, clear-cart, apply-discount, remove-discount
5584
- Validation validate-discount, calculate-shipping
5585
- Products stock-check
5586
- Schema get-collection-schema
5587
- Context get-tenant-context
5588
- Field Config list-configurable-fields, update-field-config
5589
- Guidance sdk-get-recipe, sdk-search-docs, sdk-get-auth-setup, sdk-get-collection-pattern
5590
5746
 
5591
5747
  Prompts (4): sdk-usage-guide, collection-query-help, order-flow-guide, feature-setup-guide
5592
5748
  Resources (12): config, collections-schema, getting-started, guides, api, query-builder, react-query, server-api, customer-auth, browser-vs-server, file-upload, webhook
@@ -5600,6 +5756,19 @@ SDK https://01.software/docs/sdk/client
5600
5756
  API Reference https://01.software/docs/api/rest-api
5601
5757
  Console https://console.01.software
5602
5758
  `;
5759
+ var PROTECTED_RESOURCE_METADATA = JSON.stringify({
5760
+ resource: MCP_RESOURCE_AUDIENCE2,
5761
+ authorization_servers: [MCP_OAUTH_ISSUER3],
5762
+ scopes_supported: [MCP_SCOPES2.read, MCP_SCOPES2.write]
5763
+ });
5764
+ var SERVICE_JWKS_PATH = "/.well-known/service-jwks.json";
5765
+ function writeOAuthError(res, status, error, description) {
5766
+ res.writeHead(status, {
5767
+ "Content-Type": "application/json",
5768
+ "WWW-Authenticate": `Bearer resource_metadata="${MCP_PROTECTED_RESOURCE_METADATA_PATH}", error="${error}", error_description="${description}"`
5769
+ });
5770
+ res.end(JSON.stringify({ error, error_description: description }));
5771
+ }
5603
5772
  async function handler18(req, res) {
5604
5773
  setCors(res);
5605
5774
  if (req.method === "OPTIONS") {
@@ -5608,6 +5777,30 @@ async function handler18(req, res) {
5608
5777
  return;
5609
5778
  }
5610
5779
  if (req.method === "GET") {
5780
+ const pathname = new URL(req.url ?? "/", MCP_RESOURCE_AUDIENCE2).pathname;
5781
+ if (pathname === MCP_PROTECTED_RESOURCE_METADATA_PATH) {
5782
+ res.setHeader("Access-Control-Allow-Origin", "*");
5783
+ res.writeHead(200, { "Content-Type": "application/json" });
5784
+ res.end(PROTECTED_RESOURCE_METADATA);
5785
+ return;
5786
+ }
5787
+ if (pathname === SERVICE_JWKS_PATH) {
5788
+ res.setHeader("Access-Control-Allow-Origin", "*");
5789
+ try {
5790
+ res.writeHead(200, {
5791
+ "Cache-Control": "public, max-age=60",
5792
+ "Content-Type": "application/json"
5793
+ });
5794
+ res.end(JSON.stringify(mcpServicePublicJwks()));
5795
+ } catch {
5796
+ res.writeHead(503, {
5797
+ "Cache-Control": "no-store",
5798
+ "Content-Type": "application/json"
5799
+ });
5800
+ res.end(JSON.stringify({ keys: [] }));
5801
+ }
5802
+ return;
5803
+ }
5611
5804
  res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
5612
5805
  res.end(HOME_PAGE);
5613
5806
  return;
@@ -5617,20 +5810,14 @@ async function handler18(req, res) {
5617
5810
  res.end(METHOD_NOT_ALLOWED);
5618
5811
  return;
5619
5812
  }
5620
- const apiKey = getHeaderValue(req.headers, "x-api-key");
5621
- const auth = validateApiKey(apiKey);
5813
+ const auth = await validateBearerAuthorizationHeaderAsync(
5814
+ getHeaderValue(req.headers, "authorization")
5815
+ );
5622
5816
  if (!auth.valid) {
5623
- res.writeHead(401, { "Content-Type": "application/json" });
5624
- res.end(JSON.stringify({ error: auth.error }));
5625
- return;
5626
- }
5627
- const publishableKey = getHeaderValue(req.headers, "x-publishable-key") ?? getHeaderValue(req.headers, "x-client-key");
5628
- if (!publishableKey) {
5629
- res.writeHead(401, { "Content-Type": "application/json" });
5630
- res.end(JSON.stringify({ error: "x-publishable-key header is required" }));
5817
+ writeOAuthError(res, auth.error === "insufficient_scope" ? 403 : 401, auth.error, auth.errorDescription);
5631
5818
  return;
5632
5819
  }
5633
- const server = createServer();
5820
+ const server = createServer({ toolSurface: "oauth" });
5634
5821
  const transport = new StreamableHTTPServerTransport({
5635
5822
  sessionIdGenerator: void 0
5636
5823
  });
@@ -5645,12 +5832,12 @@ async function handler18(req, res) {
5645
5832
  void close();
5646
5833
  });
5647
5834
  await server.connect(transport);
5648
- const headers2 = new Headers();
5835
+ const headers = new Headers();
5649
5836
  for (const [key, value] of Object.entries(req.headers)) {
5650
- if (typeof value === "string") headers2.set(key, value);
5651
- else if (Array.isArray(value)) headers2.set(key, value.join(", "));
5837
+ if (typeof value === "string") headers.set(key, value);
5838
+ else if (Array.isArray(value)) headers.set(key, value.join(", "));
5652
5839
  }
5653
- await requestContext.run({ headers: headers2 }, async () => {
5840
+ await requestContext.run({ headers, auth: auth.context }, async () => {
5654
5841
  try {
5655
5842
  const body = req.body ?? JSON.parse(await readBody(req));
5656
5843
  await transport.handleRequest(req, res, body);