@brainerce/mcp-server 3.12.2 → 3.12.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/http.js +172 -68
- package/dist/bin/stdio.js +136 -10
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +136 -10
- package/dist/index.mjs +136 -10
- package/package.json +1 -1
package/dist/bin/http.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
// src/bin/http.ts
|
|
5
5
|
var import_node_http = require("http");
|
|
6
|
-
var import_node_crypto = require("crypto");
|
|
7
6
|
var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
8
7
|
var import_sse = require("@modelcontextprotocol/sdk/server/sse.js");
|
|
9
8
|
|
|
@@ -220,7 +219,7 @@ async function startCheckout() {
|
|
|
220
219
|
2. Customer optionally applies coupon on cart page \u2192 \`applyCoupon(cartId, code)\`
|
|
221
220
|
3. Detect payment providers \u2192 \`getPaymentProviders()\`
|
|
222
221
|
4. Start checkout session (\`startGuestCheckout()\` or \`createCheckout()\`)
|
|
223
|
-
5. Set shipping address (includes required email) \u2192 \`setShippingAddress()\`
|
|
222
|
+
5. Set shipping address (includes required email) \u2192 \`setShippingAddress()\` \u2014 also pass \`notes\` from the **"Order notes" textarea that every checkout page should include by default** (optional field, max 2000 chars; lands on the order for the merchant)
|
|
224
223
|
6. Select shipping method \u2192 \`selectShippingMethod()\`
|
|
225
224
|
6b. (Optional) Set checkout custom fields \u2192 \`setCheckoutCustomFields()\` \u2014 surcharges auto-calculated
|
|
226
225
|
7. Create payment intent \u2192 \`createPaymentIntent()\` \u2192 returns \`{ clientSecret, provider }\`
|
|
@@ -242,6 +241,8 @@ function CheckoutPage() {
|
|
|
242
241
|
const [checkout, setCheckout] = useState<Checkout | null>(null);
|
|
243
242
|
const [error, setError] = useState<string | null>(null);
|
|
244
243
|
const [loading, setLoading] = useState(true);
|
|
244
|
+
// Optional "Order notes" textarea \u2014 render it on the checkout page by default
|
|
245
|
+
const [orderNotes, setOrderNotes] = useState('');
|
|
245
246
|
|
|
246
247
|
useEffect(() => { startCheckout(); }, []);
|
|
247
248
|
|
|
@@ -276,6 +277,7 @@ function CheckoutPage() {
|
|
|
276
277
|
postalCode: cart.shippingAddress.postalCode,
|
|
277
278
|
country: cart.shippingAddress.country,
|
|
278
279
|
phone: cart.shippingAddress.phone,
|
|
280
|
+
notes: orderNotes || undefined, // from the "Order notes" textarea \u2014 include one by default
|
|
279
281
|
});
|
|
280
282
|
setCheckout(checkoutData);
|
|
281
283
|
|
|
@@ -2126,6 +2128,7 @@ A useful order card includes ALL of the following when the data is present. Each
|
|
|
2126
2128
|
| **Tracking** | \`order.trackingNumber\`, \`order.trackingUrl\`, \`order.carrier\`, \`order.shippedAt\`, \`order.deliveredAt\` | Link out to \`trackingUrl\` when set. |
|
|
2127
2129
|
| **Payment** | \`order.paymentMethod\`, \`order.financialStatus\` | Badge \`financialStatus\` (paid / pending / refunded / partially_refunded). |
|
|
2128
2130
|
| Downloads | \`order.hasDownloads\` \u2192 \`client.getOrderDownloads(id)\` | Separate call; returns \`OrderDownloadLink[]\`. |
|
|
2131
|
+
| **Order note** | \`order.notes\` | The shopper's own checkout note, echoed back read-only. Render when present ("Your order note"). |
|
|
2129
2132
|
| Financial summary | \`order.subtotal\`, \`order.appliedDiscounts\`, \`order.couponCode\` + \`couponDiscount\`, \`order.shippingAmount\`, \`order.taxAmount\`, \`order.totalAmount\` | Breakdown rows + final total. In VAT-inclusive mode \`taxAmount\` is 0 \u2014 read \`order.taxBreakdown.totalTax\` and label "Tax (incl.)". |
|
|
2130
2133
|
|
|
2131
2134
|
#### Rendering \`order.items[i].customizations\` by type
|
|
@@ -2151,7 +2154,6 @@ The storefront scaffolded by \`create-brainerce-store\` already implements this.
|
|
|
2151
2154
|
These fields are NOT returned to buyers by \`/customers/me/orders\`. If you see them in older examples, ignore them \u2014 they are for merchant/admin views only.
|
|
2152
2155
|
|
|
2153
2156
|
- \`order.accountId\`, \`order.storeId\`, \`order.customerId\` (already known on the request)
|
|
2154
|
-
- \`order.notes\` (internal merchant notes)
|
|
2155
2157
|
- \`order.customFieldValues\` (order-level checkout fields \u2014 merchant concept; buyers see \`items[i].customizations\`)
|
|
2156
2158
|
- \`order.appliedSurcharges\`, \`order.surchargeAmount\`, \`order.appliedRuleIds\`, \`order.downloadMeta\`, \`order.pickupLocationData\``;
|
|
2157
2159
|
}
|
|
@@ -2199,7 +2201,22 @@ function OrderConfirmation() {
|
|
|
2199
2201
|
|
|
2200
2202
|
**Common mistake:** Don't just check URL params and show success immediately!
|
|
2201
2203
|
You MUST call \`waitForOrder(checkoutId)\` to verify the order was created.
|
|
2202
|
-
Also call \`handlePaymentSuccess(checkoutId)\` to clear purchased items from the cart
|
|
2204
|
+
Also call \`handlePaymentSuccess(checkoutId)\` to clear purchased items from the cart.
|
|
2205
|
+
|
|
2206
|
+
### Optional: render full order details on the confirmation page
|
|
2207
|
+
|
|
2208
|
+
Want a richer thank-you page (items, shipping address, totals, the shopper's
|
|
2209
|
+
order note)? After \`waitForOrder\` succeeds, fetch the full buyer-safe order \u2014
|
|
2210
|
+
works for guests too (possession of the checkout id is the credential):
|
|
2211
|
+
|
|
2212
|
+
\`\`\`typescript
|
|
2213
|
+
const order = await client.getOrderByCheckout(checkoutId);
|
|
2214
|
+
// order.items, order.shippingAddress, order.notes, order.subtotal,
|
|
2215
|
+
// order.shippingAmount, order.taxAmount / order.taxBreakdown, order.totalAmount
|
|
2216
|
+
\`\`\`
|
|
2217
|
+
|
|
2218
|
+
Render whatever subset fits your design \u2014 same field shapes as the buyer
|
|
2219
|
+
order-history view (see "Buyer order view" section).`;
|
|
2203
2220
|
}
|
|
2204
2221
|
function getI18nSection() {
|
|
2205
2222
|
return `## Internationalization (i18n)
|
|
@@ -2529,6 +2546,8 @@ dashboard or the admin SDK).
|
|
|
2529
2546
|
|
|
2530
2547
|
\`\`\`typescript
|
|
2531
2548
|
// Publish to a specific vibe-coded site (admin mode):
|
|
2549
|
+
await admin.publishProductToSalesChannel('prod_id', 'vc_conn_id');
|
|
2550
|
+
await admin.publishCouponToSalesChannel('coupon_id', 'vc_conn_id');
|
|
2532
2551
|
await admin.publishCategoryToVibeCodedSite('cat_id', 'conn_id');
|
|
2533
2552
|
await admin.publishTagToVibeCodedSite('tag_id', 'conn_id');
|
|
2534
2553
|
await admin.publishBrandToVibeCodedSite('brand_id', 'conn_id');
|
|
@@ -3324,11 +3343,18 @@ Points are earned automatically by the platform when an order is paid \u2014 the
|
|
|
3324
3343
|
\`\`\`typescript
|
|
3325
3344
|
// requires client.setCustomerToken(auth.token)
|
|
3326
3345
|
const status = await client.getLoyaltyStatus();
|
|
3327
|
-
// {
|
|
3346
|
+
// {
|
|
3347
|
+
// enrolled, pointsBalance, lifetimeEarned,
|
|
3348
|
+
// program: { pointsName, currencyRatio, status } | null,
|
|
3349
|
+
// tier: { id, name, level, pointsMultiplier } | null,
|
|
3350
|
+
// nextTier: { id, name, level, pointsMultiplier, qualificationType, qualificationThreshold } | null,
|
|
3351
|
+
// progressToNextTier, pointsToNextTier,
|
|
3352
|
+
// }
|
|
3328
3353
|
\`\`\`
|
|
3329
3354
|
|
|
3330
3355
|
- \`program === null\` \u2192 the store has no loyalty program; hide the loyalty UI.
|
|
3331
3356
|
- \`program.status !== 'ACTIVE'\` \u2192 program paused/draft; hide the loyalty UI.
|
|
3357
|
+
- \`tier\`/\`nextTier\` are \`null\` when the store hasn't configured tiers, or the customer hasn't qualified for one yet.
|
|
3332
3358
|
|
|
3333
3359
|
## Enroll (only if auto-enroll is off)
|
|
3334
3360
|
|
|
@@ -3336,19 +3362,67 @@ const status = await client.getLoyaltyStatus();
|
|
|
3336
3362
|
await client.enrollInLoyalty(); // same status shape; no-op if already enrolled
|
|
3337
3363
|
\`\`\`
|
|
3338
3364
|
|
|
3365
|
+
May grant a one-time "joined the program" bonus if configured \u2014 distinct from any account-signup bonus (granted automatically on registration, not by this call).
|
|
3366
|
+
|
|
3339
3367
|
## Rewards & redemption
|
|
3340
3368
|
|
|
3341
3369
|
\`\`\`typescript
|
|
3342
3370
|
const rewards = await client.getAvailableRewards();
|
|
3343
|
-
// [{ id, name, description, pointsCost, discountValue, minOrderAmount, maxUsesPerUser, isActive }]
|
|
3371
|
+
// [{ id, name, description, pointsCost, type, discountValue, minOrderAmount, maxUsesPerUser, minTierLevel, isActive }]
|
|
3372
|
+
// Already filtered to rewards the customer's current tier qualifies for.
|
|
3344
3373
|
|
|
3345
|
-
const { couponCode, discountValue, pointsBalance } =
|
|
3374
|
+
const { couponCode, discountType, discountValue, pointsBalance } =
|
|
3375
|
+
await client.redeemLoyaltyReward(rewardId);
|
|
3346
3376
|
await client.applyCoupon(cartId, couponCode); // applies via the normal coupon engine
|
|
3347
3377
|
\`\`\`
|
|
3348
3378
|
|
|
3349
|
-
- \`
|
|
3350
|
-
-
|
|
3379
|
+
- \`reward.type\` is \`'FIXED_DISCOUNT'\` (currency amount) or \`'PERCENT_DISCOUNT'\` (0-100 percent) \u2014 the redeem result's \`discountType\` matches.
|
|
3380
|
+
- \`redeemLoyaltyReward\` mints a one-time coupon (\`usageLimit: 1\`). Apply it with the normal \`applyCoupon\` flow.
|
|
3381
|
+
- Throws if the customer has insufficient points, or their tier is below \`reward.minTierLevel\`. If minting fails, points are refunded automatically.
|
|
3351
3382
|
- Disable a reward's redeem button when \`pointsBalance < reward.pointsCost\`.
|
|
3383
|
+
|
|
3384
|
+
## Social share (optional)
|
|
3385
|
+
|
|
3386
|
+
\`\`\`typescript
|
|
3387
|
+
await client.reportSocialShare('instagram'); // platform optional, self-reported, once per customer
|
|
3388
|
+
\`\`\`
|
|
3389
|
+
|
|
3390
|
+
## Referrals (when the store enables them)
|
|
3391
|
+
|
|
3392
|
+
\`getLoyaltyStatus()\` also returns \`referralCode\` (the member's share code, null when referrals are disabled or the customer isn't enrolled) and \`referralWelcomeCoupon\` (the still-unused welcome coupon this customer got for signing up via a referral link).
|
|
3393
|
+
|
|
3394
|
+
\`\`\`typescript
|
|
3395
|
+
// 1. Referrer: build a share link around their code.
|
|
3396
|
+
const { referralCode } = await client.getLoyaltyStatus();
|
|
3397
|
+
const shareUrl = \`https://your-store.com/?ref=\${referralCode}\`;
|
|
3398
|
+
|
|
3399
|
+
// 2. Landing page: PUBLIC lookup \u2014 no customerToken needed.
|
|
3400
|
+
const info = await client.getReferralInfo(refFromQuery);
|
|
3401
|
+
// { valid, referrerFirstName, reward: { name, type, value, minOrderAmount } | null }
|
|
3402
|
+
if (info.valid) {
|
|
3403
|
+
// "Jane sent you a gift: 10% off your first order"
|
|
3404
|
+
}
|
|
3405
|
+
|
|
3406
|
+
// 3. Registration: pass the code \u2014 invalid codes never fail registration
|
|
3407
|
+
// (validated asynchronously server-side with fraud checks).
|
|
3408
|
+
await client.registerCustomer({ email, password, referralCode: refFromQuery });
|
|
3409
|
+
|
|
3410
|
+
// 4. Show the referee their welcome coupon after login:
|
|
3411
|
+
const { referralWelcomeCoupon } = await client.getLoyaltyStatus();
|
|
3412
|
+
if (referralWelcomeCoupon) {
|
|
3413
|
+
await client.applyCoupon(cartId, referralWelcomeCoupon.code);
|
|
3414
|
+
}
|
|
3415
|
+
\`\`\`
|
|
3416
|
+
|
|
3417
|
+
The referrer earns a points bonus after the referee's first qualifying order (held through the program's pending window, like order points). Everything server-side \u2014 no storefront earn calls.
|
|
3418
|
+
|
|
3419
|
+
## Birthday gifts (when the store enables them)
|
|
3420
|
+
|
|
3421
|
+
No dedicated calls \u2014 collect \`birthMonth\`/\`birthDay\` (1-12 / 1-31, **no year**, must be sent together) via \`updateMyProfile()\` and the platform emails a one-time gift coupon ahead of the customer's birthday.
|
|
3422
|
+
|
|
3423
|
+
\`\`\`typescript
|
|
3424
|
+
await client.updateMyProfile({ birthMonth: 4, birthDay: 17 });
|
|
3425
|
+
\`\`\`
|
|
3352
3426
|
`;
|
|
3353
3427
|
}
|
|
3354
3428
|
function getSectionByTopic(topic, connectionId, currency) {
|
|
@@ -3873,6 +3947,7 @@ interface Checkout {
|
|
|
3873
3947
|
appliedSurcharges?: Array<{ key: string; name: string; value: unknown; amount: string }> | null;
|
|
3874
3948
|
customFieldValues?: Record<string, unknown> | null;
|
|
3875
3949
|
couponCode?: string | null;
|
|
3950
|
+
notes?: string | null; // Order-level shopper note \u2014 copied onto the order at completion
|
|
3876
3951
|
lineItems: CheckoutLineItem[]; // Use THIS for order summary, NOT cart.items!
|
|
3877
3952
|
itemCount: number;
|
|
3878
3953
|
availableShippingRates?: ShippingRate[];
|
|
@@ -3941,6 +4016,7 @@ interface SetShippingAddressDto {
|
|
|
3941
4016
|
postalCode: string;
|
|
3942
4017
|
country: string;
|
|
3943
4018
|
phone?: string;
|
|
4019
|
+
notes?: string; // Order notes textarea \u2014 include one on checkout by default! Max 2000 chars, lands on the order
|
|
3944
4020
|
}
|
|
3945
4021
|
|
|
3946
4022
|
interface CreateCheckoutDto {
|
|
@@ -4884,6 +4960,18 @@ interface TaxEstimateResponse {
|
|
|
4884
4960
|
// full shipping address. The country comes from your edge runtime
|
|
4885
4961
|
// (CF-IPCountry / request.geo.country / etc.), NOT the request IP.`;
|
|
4886
4962
|
var LOYALTY_TYPES = `// ---- Loyalty & Rewards (storefront mode only) ----
|
|
4963
|
+
interface LoyaltyTierSummary {
|
|
4964
|
+
id: string;
|
|
4965
|
+
name: string;
|
|
4966
|
+
level: number; // ordering; higher = better tier
|
|
4967
|
+
pointsMultiplier: number;
|
|
4968
|
+
}
|
|
4969
|
+
|
|
4970
|
+
interface LoyaltyNextTierSummary extends LoyaltyTierSummary {
|
|
4971
|
+
qualificationType: 'SPEND' | 'POINTS';
|
|
4972
|
+
qualificationThreshold: number;
|
|
4973
|
+
}
|
|
4974
|
+
|
|
4887
4975
|
interface LoyaltyStatus {
|
|
4888
4976
|
enrolled: boolean;
|
|
4889
4977
|
pointsBalance: number; // redeemable points (pending earns excluded)
|
|
@@ -4893,6 +4981,28 @@ interface LoyaltyStatus {
|
|
|
4893
4981
|
currencyRatio: number; // points needed to redeem 1 unit of store currency
|
|
4894
4982
|
status: 'DRAFT' | 'ACTIVE' | 'PAUSED';
|
|
4895
4983
|
} | null; // null when the store has no loyalty program
|
|
4984
|
+
tier: LoyaltyTierSummary | null; // null if untiered / no tiers configured
|
|
4985
|
+
nextTier: LoyaltyNextTierSummary | null; // null if at the top tier already
|
|
4986
|
+
progressToNextTier: number; // 0..1
|
|
4987
|
+
pointsToNextTier: number | null;
|
|
4988
|
+
referralCode?: string | null; // member's share code; null when referrals disabled / not enrolled
|
|
4989
|
+
referralWelcomeCoupon?: { // unused welcome coupon from signing up via a referral link
|
|
4990
|
+
code: string;
|
|
4991
|
+
type: string; // 'PERCENTAGE' | 'FIXED_AMOUNT'
|
|
4992
|
+
value: number;
|
|
4993
|
+
} | null;
|
|
4994
|
+
}
|
|
4995
|
+
|
|
4996
|
+
// Public referral-link lookup \u2014 NO customerToken needed (safe pre-signup)
|
|
4997
|
+
interface ReferralInfo {
|
|
4998
|
+
valid: boolean; // false for unknown/disabled codes
|
|
4999
|
+
referrerFirstName: string | null; // for "Jane sent you a gift" copy \u2014 no other PII
|
|
5000
|
+
reward: {
|
|
5001
|
+
name: string;
|
|
5002
|
+
type: 'FIXED_DISCOUNT' | 'PERCENT_DISCOUNT';
|
|
5003
|
+
value: number;
|
|
5004
|
+
minOrderAmount: number | null;
|
|
5005
|
+
} | null;
|
|
4896
5006
|
}
|
|
4897
5007
|
|
|
4898
5008
|
interface LoyaltyReward {
|
|
@@ -4900,16 +5010,19 @@ interface LoyaltyReward {
|
|
|
4900
5010
|
name: string;
|
|
4901
5011
|
description: string | null;
|
|
4902
5012
|
pointsCost: number;
|
|
4903
|
-
|
|
5013
|
+
type: 'FIXED_DISCOUNT' | 'PERCENT_DISCOUNT';
|
|
5014
|
+
discountValue: number; // currency amount (FIXED_DISCOUNT) or 0-100 percent (PERCENT_DISCOUNT)
|
|
4904
5015
|
minOrderAmount: number | null;
|
|
4905
5016
|
maxUsesPerUser: number;
|
|
4906
5017
|
isActive: boolean;
|
|
5018
|
+
minTierLevel: number | null; // minimum tier level required, or null for no restriction
|
|
4907
5019
|
createdAt: string;
|
|
4908
5020
|
updatedAt: string;
|
|
4909
5021
|
}
|
|
4910
5022
|
|
|
4911
5023
|
interface RedeemRewardResult {
|
|
4912
5024
|
couponCode: string; // one-time coupon to apply at checkout
|
|
5025
|
+
discountType: 'FIXED_DISCOUNT' | 'PERCENT_DISCOUNT';
|
|
4913
5026
|
discountValue: number;
|
|
4914
5027
|
pointsSpent: number;
|
|
4915
5028
|
pointsBalance: number; // remaining balance after redemption
|
|
@@ -4919,6 +5032,10 @@ interface RedeemRewardResult {
|
|
|
4919
5032
|
// client.enrollInLoyalty(): Promise<LoyaltyStatus>
|
|
4920
5033
|
// client.getAvailableRewards(): Promise<LoyaltyReward[]>
|
|
4921
5034
|
// client.redeemLoyaltyReward(rewardId: string): Promise<RedeemRewardResult>
|
|
5035
|
+
// client.reportSocialShare(platform?: string): Promise<{ awarded: boolean; points: number }>
|
|
5036
|
+
// client.getReferralInfo(code: string): Promise<ReferralInfo> // PUBLIC \u2014 no customerToken
|
|
5037
|
+
// Referral signup: client.registerCustomer({ ..., referralCode })
|
|
5038
|
+
// Birthday gift data: client.updateMyProfile({ birthMonth, birthDay }) // 1-12 / 1-31, no year, both together
|
|
4922
5039
|
`;
|
|
4923
5040
|
var TYPES_BY_DOMAIN = {
|
|
4924
5041
|
products: PRODUCTS_TYPES,
|
|
@@ -6417,6 +6534,14 @@ function formatCapabilities(caps) {
|
|
|
6417
6534
|
lines.push(
|
|
6418
6535
|
caps.features.hasLoyaltyProgram ? "\u2705 Loyalty program active (points & rewards \u2014 storefront-mode only)" : "\u274C No loyalty program"
|
|
6419
6536
|
);
|
|
6537
|
+
if (caps.features.hasLoyaltyProgram) {
|
|
6538
|
+
lines.push(
|
|
6539
|
+
caps.features.hasReferralProgram ? "\u2705 Referral program enabled (share codes + welcome reward \u2014 storefront-mode only)" : "\u274C Referral program not enabled"
|
|
6540
|
+
);
|
|
6541
|
+
lines.push(
|
|
6542
|
+
caps.features.hasBirthdayRewards ? "\u2705 Birthday gifts enabled (set birthMonth/birthDay via updateMyProfile)" : "\u274C Birthday gifts not enabled"
|
|
6543
|
+
);
|
|
6544
|
+
}
|
|
6420
6545
|
lines.push("");
|
|
6421
6546
|
lines.push("## Connection Settings");
|
|
6422
6547
|
lines.push(
|
|
@@ -6464,7 +6589,7 @@ function formatCapabilities(caps) {
|
|
|
6464
6589
|
}
|
|
6465
6590
|
if (caps.features.hasLoyaltyProgram) {
|
|
6466
6591
|
suggestions.push(
|
|
6467
|
-
"A loyalty program is active. In a storefront-mode client (new BrainerceClient({ storeId })), show the logged-in customer their points balance and redeemable rewards, and let them redeem for a coupon at checkout. Use getLoyaltyStatus(), getAvailableRewards(),
|
|
6592
|
+
"A loyalty program is active. In a storefront-mode client (new BrainerceClient({ storeId })), show the logged-in customer their points balance, tier progress, and redeemable rewards, and let them redeem for a coupon at checkout. Use getLoyaltyStatus() (includes tier/nextTier/progressToNextTier), getAvailableRewards(), redeemLoyaltyReward(id), and optionally reportSocialShare(). Note: loyalty is NOT available in vibe-coded mode."
|
|
6468
6593
|
);
|
|
6469
6594
|
}
|
|
6470
6595
|
if (caps.features.hasCheckoutCustomFields) {
|
|
@@ -7602,69 +7727,29 @@ var SSE_KEEPALIVE_MS = 1e4;
|
|
|
7602
7727
|
var SESSION_MAX_AGE_MS = 4 * 60 * 60 * 1e3;
|
|
7603
7728
|
var NODE_ENV = process.env.NODE_ENV || "development";
|
|
7604
7729
|
var IS_DEV = NODE_ENV === "development";
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
"FATAL: MCP_AUTH_TOKENS is not set. The MCP HTTP server must run with at least one Bearer token in non-development environments.\nMint a token (e.g., `mcp_<random>`) via the dashboard and add it (comma-separated) to MCP_AUTH_TOKENS."
|
|
7609
|
-
);
|
|
7610
|
-
process.exit(1);
|
|
7611
|
-
}
|
|
7612
|
-
if (AUTH_TOKENS.length === 0 && IS_DEV) {
|
|
7613
|
-
console.warn(
|
|
7614
|
-
"WARN: MCP_AUTH_TOKENS is not set. Running in development mode \u2014 any non-empty Bearer token will be accepted."
|
|
7730
|
+
if (process.env.MCP_AUTH_TOKENS) {
|
|
7731
|
+
console.info(
|
|
7732
|
+
"NOTE: MCP_AUTH_TOKENS is set but ignored \u2014 the docs MCP server is public by design. The env var can be removed."
|
|
7615
7733
|
);
|
|
7616
7734
|
}
|
|
7617
7735
|
var ALLOWED_ORIGINS = (process.env.MCP_ALLOWED_ORIGINS || "").split(",").map((o) => o.trim()).filter((o) => o.length > 0);
|
|
7618
7736
|
var LOCALHOST_ORIGIN_RE = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/;
|
|
7619
7737
|
var RATE_LIMIT_WINDOW_MS = parseInt(process.env.MCP_RATE_LIMIT_WINDOW_MS || "60000", 10);
|
|
7620
7738
|
var RATE_LIMIT_MAX = parseInt(process.env.MCP_RATE_LIMIT_MAX || "60", 10);
|
|
7739
|
+
var RATE_BUCKETS_MAX = parseInt(process.env.MCP_RATE_BUCKETS_MAX || "20000", 10);
|
|
7621
7740
|
var rateBuckets = /* @__PURE__ */ new Map();
|
|
7741
|
+
var MAX_SSE_SESSIONS = parseInt(process.env.MCP_MAX_SSE_SESSIONS || "500", 10);
|
|
7742
|
+
var MAX_SSE_SESSIONS_PER_IP = parseInt(process.env.MCP_MAX_SSE_SESSIONS_PER_IP || "5", 10);
|
|
7622
7743
|
var MAX_BODY_BYTES = parseInt(process.env.MCP_MAX_BODY_BYTES || String(100 * 1024), 10);
|
|
7623
|
-
function safeEqual(a, b) {
|
|
7624
|
-
if (a.length !== b.length) {
|
|
7625
|
-
const max = Math.max(a.length, b.length);
|
|
7626
|
-
const ax = Buffer.alloc(max);
|
|
7627
|
-
const bx = Buffer.alloc(max);
|
|
7628
|
-
a.copy(ax);
|
|
7629
|
-
b.copy(bx);
|
|
7630
|
-
(0, import_node_crypto.timingSafeEqual)(ax, bx);
|
|
7631
|
-
return false;
|
|
7632
|
-
}
|
|
7633
|
-
return (0, import_node_crypto.timingSafeEqual)(a, b);
|
|
7634
|
-
}
|
|
7635
|
-
function authenticate(req, res) {
|
|
7636
|
-
const header = req.headers["authorization"];
|
|
7637
|
-
if (typeof header !== "string" || !header.toLowerCase().startsWith("bearer ")) {
|
|
7638
|
-
writeJson(res, 401, {
|
|
7639
|
-
error: "Missing or malformed Authorization header. Expected: Bearer <token>."
|
|
7640
|
-
});
|
|
7641
|
-
return false;
|
|
7642
|
-
}
|
|
7643
|
-
const presented = header.slice(7).trim();
|
|
7644
|
-
if (presented.length === 0) {
|
|
7645
|
-
writeJson(res, 401, { error: "Empty Bearer token." });
|
|
7646
|
-
return false;
|
|
7647
|
-
}
|
|
7648
|
-
if (AUTH_TOKENS.length === 0 && IS_DEV) {
|
|
7649
|
-
return true;
|
|
7650
|
-
}
|
|
7651
|
-
const presentedBuf = Buffer.from(presented, "utf8");
|
|
7652
|
-
let matched = false;
|
|
7653
|
-
for (const token of AUTH_TOKENS) {
|
|
7654
|
-
if (safeEqual(presentedBuf, token)) {
|
|
7655
|
-
matched = true;
|
|
7656
|
-
}
|
|
7657
|
-
}
|
|
7658
|
-
if (!matched) {
|
|
7659
|
-
writeJson(res, 401, { error: "Invalid Bearer token." });
|
|
7660
|
-
return false;
|
|
7661
|
-
}
|
|
7662
|
-
return true;
|
|
7663
|
-
}
|
|
7664
7744
|
function clientIp(req) {
|
|
7745
|
+
const cf = req.headers["cf-connecting-ip"];
|
|
7746
|
+
if (typeof cf === "string" && cf.length > 0) {
|
|
7747
|
+
return cf.trim();
|
|
7748
|
+
}
|
|
7665
7749
|
const xff = req.headers["x-forwarded-for"];
|
|
7666
7750
|
if (typeof xff === "string" && xff.length > 0) {
|
|
7667
|
-
|
|
7751
|
+
const parts = xff.split(",");
|
|
7752
|
+
return parts[parts.length - 1].trim();
|
|
7668
7753
|
}
|
|
7669
7754
|
return req.socket.remoteAddress || "unknown";
|
|
7670
7755
|
}
|
|
@@ -7673,6 +7758,9 @@ function rateLimit(req, res) {
|
|
|
7673
7758
|
const now = Date.now();
|
|
7674
7759
|
const bucket = rateBuckets.get(ip);
|
|
7675
7760
|
if (!bucket || bucket.resetAt <= now) {
|
|
7761
|
+
if (rateBuckets.size >= RATE_BUCKETS_MAX) {
|
|
7762
|
+
rateBuckets.clear();
|
|
7763
|
+
}
|
|
7676
7764
|
rateBuckets.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
|
|
7677
7765
|
return true;
|
|
7678
7766
|
}
|
|
@@ -7796,7 +7884,6 @@ async function main() {
|
|
|
7796
7884
|
return;
|
|
7797
7885
|
}
|
|
7798
7886
|
if (!rateLimit(req, res)) return;
|
|
7799
|
-
if (!authenticate(req, res)) return;
|
|
7800
7887
|
if (req.url === "/mcp") {
|
|
7801
7888
|
try {
|
|
7802
7889
|
const server = createServer();
|
|
@@ -7815,6 +7902,19 @@ async function main() {
|
|
|
7815
7902
|
return;
|
|
7816
7903
|
}
|
|
7817
7904
|
if (req.url === "/sse" && req.method === "GET") {
|
|
7905
|
+
if (sseSessions.size >= MAX_SSE_SESSIONS) {
|
|
7906
|
+
writeJson(res, 503, { error: "Server at capacity. Try again later." });
|
|
7907
|
+
return;
|
|
7908
|
+
}
|
|
7909
|
+
const ip = clientIp(req);
|
|
7910
|
+
let ownedByIp = 0;
|
|
7911
|
+
for (const session of sseSessions.values()) {
|
|
7912
|
+
if (session.ip === ip) ownedByIp += 1;
|
|
7913
|
+
}
|
|
7914
|
+
if (ownedByIp >= MAX_SSE_SESSIONS_PER_IP) {
|
|
7915
|
+
writeJson(res, 429, { error: "Too many concurrent sessions from this address." });
|
|
7916
|
+
return;
|
|
7917
|
+
}
|
|
7818
7918
|
try {
|
|
7819
7919
|
res.setHeader("X-Accel-Buffering", "no");
|
|
7820
7920
|
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
@@ -7836,7 +7936,8 @@ async function main() {
|
|
|
7836
7936
|
transport,
|
|
7837
7937
|
res,
|
|
7838
7938
|
keepAliveTimer,
|
|
7839
|
-
createdAt: Date.now()
|
|
7939
|
+
createdAt: Date.now(),
|
|
7940
|
+
ip
|
|
7840
7941
|
});
|
|
7841
7942
|
transport.onclose = () => {
|
|
7842
7943
|
cleanupSession(sessionId);
|
|
@@ -7906,20 +8007,23 @@ ${signal} received \u2014 shutting down gracefully...`);
|
|
|
7906
8007
|
}
|
|
7907
8008
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
7908
8009
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
8010
|
+
httpServer.headersTimeout = 15e3;
|
|
8011
|
+
httpServer.requestTimeout = 6e4;
|
|
7909
8012
|
httpServer.listen(PORT, () => {
|
|
7910
8013
|
console.info(`Brainerce MCP server running on http://localhost:${PORT}`);
|
|
7911
8014
|
console.info(` Streamable HTTP: http://localhost:${PORT}/mcp`);
|
|
7912
8015
|
console.info(` Legacy SSE: http://localhost:${PORT}/sse`);
|
|
7913
8016
|
console.info(` Health check: http://localhost:${PORT}/health`);
|
|
7914
|
-
console.info(
|
|
7915
|
-
` Auth tokens: ${AUTH_TOKENS.length} configured${AUTH_TOKENS.length === 0 && IS_DEV ? " (dev: any non-empty Bearer accepted)" : ""}`
|
|
7916
|
-
);
|
|
8017
|
+
console.info(" Auth: none (public docs server)");
|
|
7917
8018
|
console.info(
|
|
7918
8019
|
` CORS: ${ALLOWED_ORIGINS.length} allow-listed origin(s)${IS_DEV ? " + localhost (dev)" : ""}`
|
|
7919
8020
|
);
|
|
7920
8021
|
console.info(
|
|
7921
8022
|
` Rate limit: ${RATE_LIMIT_MAX} req / ${Math.round(RATE_LIMIT_WINDOW_MS / 1e3)}s per IP`
|
|
7922
8023
|
);
|
|
8024
|
+
console.info(
|
|
8025
|
+
` SSE sessions: max ${MAX_SSE_SESSIONS} total, ${MAX_SSE_SESSIONS_PER_IP} per IP`
|
|
8026
|
+
);
|
|
7923
8027
|
console.info(` Max body: ${MAX_BODY_BYTES} bytes`);
|
|
7924
8028
|
});
|
|
7925
8029
|
}
|