@toromarket/mcp-server 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +85 -317
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +83 -315
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -65,7 +65,9 @@ var placeOrderSchema = z.object({
|
|
|
65
65
|
side: z.enum(["BUY", "SELL"]),
|
|
66
66
|
type: z.enum(["LIMIT", "MARKET"]).default("LIMIT"),
|
|
67
67
|
price: z.coerce.number().min(0).max(1).optional(),
|
|
68
|
-
quantity: z.coerce.number().positive().max(1e5)
|
|
68
|
+
quantity: z.coerce.number().positive().max(1e5),
|
|
69
|
+
reasoning: sanitized(1e3),
|
|
70
|
+
confidence: z.coerce.number().min(0).max(1)
|
|
69
71
|
});
|
|
70
72
|
var cancelOrderSchema = z.object({
|
|
71
73
|
marketId: id,
|
|
@@ -74,7 +76,9 @@ var cancelOrderSchema = z.object({
|
|
|
74
76
|
var tradeCryptoSchema = z.object({
|
|
75
77
|
symbol: sanitized(20),
|
|
76
78
|
side: z.enum(["BUY", "SELL"]),
|
|
77
|
-
quantity: z.coerce.number().positive().max(1e5)
|
|
79
|
+
quantity: z.coerce.number().positive().max(1e5),
|
|
80
|
+
reasoning: sanitized(1e3),
|
|
81
|
+
confidence: z.coerce.number().min(0).max(1)
|
|
78
82
|
});
|
|
79
83
|
var postMarketCommentSchema = z.object({
|
|
80
84
|
marketId: id,
|
|
@@ -196,9 +200,11 @@ var PHASE1_TOOLS = [
|
|
|
196
200
|
type: "number",
|
|
197
201
|
description: "Price per share (0-1). Required for LIMIT orders. Ignored for MARKET orders."
|
|
198
202
|
},
|
|
199
|
-
quantity: { type: "number", description: "Share quantity" }
|
|
203
|
+
quantity: { type: "number", description: "Share quantity" },
|
|
204
|
+
reasoning: { type: "string", description: "Why you are making this trade \u2014 your thesis and key signals" },
|
|
205
|
+
confidence: { type: "number", description: "Confidence level 0-1 in this trade decision" }
|
|
200
206
|
},
|
|
201
|
-
required: ["marketId", "outcomeId", "side", "quantity"]
|
|
207
|
+
required: ["marketId", "outcomeId", "side", "quantity", "reasoning", "confidence"]
|
|
202
208
|
}
|
|
203
209
|
},
|
|
204
210
|
{
|
|
@@ -247,9 +253,11 @@ var PHASE1_TOOLS = [
|
|
|
247
253
|
properties: {
|
|
248
254
|
symbol: { type: "string", description: "Symbol like BTC" },
|
|
249
255
|
side: { type: "string", enum: ["BUY", "SELL"] },
|
|
250
|
-
quantity: { type: "number", description: "Order size" }
|
|
256
|
+
quantity: { type: "number", description: "Order size" },
|
|
257
|
+
reasoning: { type: "string", description: "Why you are making this trade \u2014 your thesis and key signals" },
|
|
258
|
+
confidence: { type: "number", description: "Confidence level 0-1 in this trade decision" }
|
|
251
259
|
},
|
|
252
|
-
required: ["symbol", "side", "quantity"]
|
|
260
|
+
required: ["symbol", "side", "quantity", "reasoning", "confidence"]
|
|
253
261
|
}
|
|
254
262
|
}
|
|
255
263
|
];
|
|
@@ -617,13 +625,17 @@ var placeFundOrderSchema = z.object({
|
|
|
617
625
|
side: z.enum(["BUY", "SELL"]),
|
|
618
626
|
type: z.enum(["LIMIT", "MARKET"]).default("LIMIT"),
|
|
619
627
|
price: z.coerce.number().min(0).max(1).optional(),
|
|
620
|
-
quantity: z.coerce.number().positive().max(1e5)
|
|
628
|
+
quantity: z.coerce.number().positive().max(1e5),
|
|
629
|
+
reasoning: sanitized(1e3),
|
|
630
|
+
confidence: z.coerce.number().min(0).max(1)
|
|
621
631
|
});
|
|
622
632
|
var tradeFundCryptoSchema = z.object({
|
|
623
633
|
fundId: id,
|
|
624
634
|
symbol: sanitized(20),
|
|
625
635
|
side: z.enum(["BUY", "SELL"]),
|
|
626
|
-
quantity: z.coerce.number().positive().max(1e5)
|
|
636
|
+
quantity: z.coerce.number().positive().max(1e5),
|
|
637
|
+
reasoning: sanitized(1e3),
|
|
638
|
+
confidence: z.coerce.number().min(0).max(1)
|
|
627
639
|
});
|
|
628
640
|
var FUND_TRADING_TOOLS = [
|
|
629
641
|
{
|
|
@@ -645,9 +657,11 @@ var FUND_TRADING_TOOLS = [
|
|
|
645
657
|
type: "number",
|
|
646
658
|
description: "Price per share (0-1). Required for LIMIT orders. Ignored for MARKET orders."
|
|
647
659
|
},
|
|
648
|
-
quantity: { type: "number", description: "Share quantity" }
|
|
660
|
+
quantity: { type: "number", description: "Share quantity" },
|
|
661
|
+
reasoning: { type: "string", description: "Why you are making this trade \u2014 your thesis and key signals" },
|
|
662
|
+
confidence: { type: "number", description: "Confidence level 0-1 in this trade decision" }
|
|
649
663
|
},
|
|
650
|
-
required: ["fundId", "marketId", "outcomeId", "side", "quantity"]
|
|
664
|
+
required: ["fundId", "marketId", "outcomeId", "side", "quantity", "reasoning", "confidence"]
|
|
651
665
|
}
|
|
652
666
|
},
|
|
653
667
|
{
|
|
@@ -659,9 +673,11 @@ var FUND_TRADING_TOOLS = [
|
|
|
659
673
|
fundId: { type: "string", description: "Your fund ID (from list_funds myFundId)" },
|
|
660
674
|
symbol: { type: "string", description: "Symbol like BTC" },
|
|
661
675
|
side: { type: "string", enum: ["BUY", "SELL"] },
|
|
662
|
-
quantity: { type: "number", description: "Order size" }
|
|
676
|
+
quantity: { type: "number", description: "Order size" },
|
|
677
|
+
reasoning: { type: "string", description: "Why you are making this trade \u2014 your thesis and key signals" },
|
|
678
|
+
confidence: { type: "number", description: "Confidence level 0-1 in this trade decision" }
|
|
663
679
|
},
|
|
664
|
-
required: ["fundId", "symbol", "side", "quantity"]
|
|
680
|
+
required: ["fundId", "symbol", "side", "quantity", "reasoning", "confidence"]
|
|
665
681
|
}
|
|
666
682
|
},
|
|
667
683
|
{
|
|
@@ -2488,7 +2504,9 @@ async function executeTool(client, baseUrl2, name, rawArgs, options) {
|
|
|
2488
2504
|
side: input.side,
|
|
2489
2505
|
type: input.type,
|
|
2490
2506
|
price: Number(price),
|
|
2491
|
-
quantity: Number(input.quantity)
|
|
2507
|
+
quantity: Number(input.quantity),
|
|
2508
|
+
reasoning: input.reasoning,
|
|
2509
|
+
confidence: input.confidence
|
|
2492
2510
|
});
|
|
2493
2511
|
const remainingBalance = await fetchRemainingBalance(client);
|
|
2494
2512
|
return { ...result, remainingBalance };
|
|
@@ -2515,7 +2533,13 @@ async function executeTool(client, baseUrl2, name, rawArgs, options) {
|
|
|
2515
2533
|
}
|
|
2516
2534
|
case "trade_crypto": {
|
|
2517
2535
|
const input = tradeCryptoSchema.parse(args);
|
|
2518
|
-
const result = await client.portfolio.trade(
|
|
2536
|
+
const result = await client.portfolio.trade({
|
|
2537
|
+
symbol: input.symbol,
|
|
2538
|
+
side: input.side,
|
|
2539
|
+
quantity: input.quantity,
|
|
2540
|
+
reasoning: input.reasoning,
|
|
2541
|
+
confidence: input.confidence
|
|
2542
|
+
});
|
|
2519
2543
|
const remainingBalance = await fetchRemainingBalance(client);
|
|
2520
2544
|
return { ...result, remainingBalance };
|
|
2521
2545
|
}
|
|
@@ -2709,7 +2733,9 @@ async function executeTool(client, baseUrl2, name, rawArgs, options) {
|
|
|
2709
2733
|
side: input.side,
|
|
2710
2734
|
type: input.type,
|
|
2711
2735
|
price,
|
|
2712
|
-
quantity: input.quantity
|
|
2736
|
+
quantity: input.quantity,
|
|
2737
|
+
reasoning: input.reasoning,
|
|
2738
|
+
confidence: input.confidence
|
|
2713
2739
|
});
|
|
2714
2740
|
const fundOrderBalance = await fetchRemainingBalance(client);
|
|
2715
2741
|
return { ...fundOrderResult, remainingBalance: fundOrderBalance };
|
|
@@ -2719,7 +2745,9 @@ async function executeTool(client, baseUrl2, name, rawArgs, options) {
|
|
|
2719
2745
|
const fundTradeResult = await client.funds.tradeCrypto(input.fundId, {
|
|
2720
2746
|
symbol: input.symbol,
|
|
2721
2747
|
side: input.side,
|
|
2722
|
-
quantity: input.quantity
|
|
2748
|
+
quantity: input.quantity,
|
|
2749
|
+
reasoning: input.reasoning,
|
|
2750
|
+
confidence: input.confidence
|
|
2723
2751
|
});
|
|
2724
2752
|
const fundTradeBalance = await fetchRemainingBalance(client);
|
|
2725
2753
|
return { ...fundTradeResult, remainingBalance: fundTradeBalance };
|
|
@@ -2955,17 +2983,11 @@ async function executeTool(client, baseUrl2, name, rawArgs, options) {
|
|
|
2955
2983
|
}
|
|
2956
2984
|
case "get_compliance_status": {
|
|
2957
2985
|
const me = await client.auth.me();
|
|
2958
|
-
const
|
|
2959
|
-
enabled: false,
|
|
2960
|
-
flags: { velocityWarning: false, regionBlocked: false, largeTransaction: false },
|
|
2961
|
-
velocity: { tradesLastMinute: 0, sessionVolume: 0 },
|
|
2962
|
-
region: null,
|
|
2963
|
-
blockedRegions: []
|
|
2964
|
-
};
|
|
2986
|
+
const compliance = await client.auth.complianceStatus();
|
|
2965
2987
|
return {
|
|
2966
2988
|
trustTier: me.trustTier,
|
|
2967
2989
|
trustScore: me.trustScore,
|
|
2968
|
-
compliance
|
|
2990
|
+
compliance
|
|
2969
2991
|
};
|
|
2970
2992
|
}
|
|
2971
2993
|
// Registry tools
|
|
@@ -3334,6 +3356,18 @@ function classifyError(error) {
|
|
|
3334
3356
|
if (error.code === "NETWORK_ERROR") {
|
|
3335
3357
|
return { error: error.message, statusCode: 0, retryable: true, recovery: "Toromarket API is unreachable. Check that the server is running." };
|
|
3336
3358
|
}
|
|
3359
|
+
if (error.code === "SPENDING_LIMIT") {
|
|
3360
|
+
return { error: error.message, statusCode: 403, retryable: false, recovery: "Daily spending limit reached. Read-only tools still work. Call get_upgrade_url to increase your limit." };
|
|
3361
|
+
}
|
|
3362
|
+
if (error.code === "SPOOFING_DETECTED") {
|
|
3363
|
+
return { error: error.message, statusCode: 403, retryable: false, recovery: "Suspicious trading pattern detected (high cancel ratio). Slow down and wait 30 seconds." };
|
|
3364
|
+
}
|
|
3365
|
+
if (error.code === "REGISTRATION_THROTTLE") {
|
|
3366
|
+
return { error: error.message, statusCode: 403, retryable: false, recovery: "Registration limit reached for this IP. Use authenticate to log in to an existing account." };
|
|
3367
|
+
}
|
|
3368
|
+
if (error.code === "RATE_LIMITED") {
|
|
3369
|
+
return { error: error.message, statusCode: 429, retryable: true, recovery: "Rate limit exceeded. Wait for the indicated cooldown period, or call get_upgrade_url to increase your tier." };
|
|
3370
|
+
}
|
|
3337
3371
|
if (status >= 500) {
|
|
3338
3372
|
return { error: error.message, statusCode: status, retryable: true, recovery: "Server error. Try again in a few seconds." };
|
|
3339
3373
|
}
|
|
@@ -3667,28 +3701,8 @@ function createLogger(level = "info") {
|
|
|
3667
3701
|
};
|
|
3668
3702
|
}
|
|
3669
3703
|
|
|
3670
|
-
// src/middleware/registration.ts
|
|
3671
|
-
import { ToromarketError as ToromarketError3 } from "@toromarket/sdk";
|
|
3672
|
-
var RegistrationThrottle = class {
|
|
3673
|
-
hasRegistered = false;
|
|
3674
|
-
async beforeExecute(toolName, _args) {
|
|
3675
|
-
if (toolName === "register_agent" && this.hasRegistered) {
|
|
3676
|
-
throw new ToromarketError3(
|
|
3677
|
-
"Only one account can be registered per session. Use authenticate to log in to an existing account.",
|
|
3678
|
-
403,
|
|
3679
|
-
"REGISTRATION_THROTTLE"
|
|
3680
|
-
);
|
|
3681
|
-
}
|
|
3682
|
-
}
|
|
3683
|
-
async afterExecute(toolName, _args, _result, error) {
|
|
3684
|
-
if (toolName === "register_agent" && !error) {
|
|
3685
|
-
this.hasRegistered = true;
|
|
3686
|
-
}
|
|
3687
|
-
}
|
|
3688
|
-
};
|
|
3689
|
-
|
|
3690
3704
|
// src/middleware/rate-limiter.ts
|
|
3691
|
-
import { ToromarketError as
|
|
3705
|
+
import { ToromarketError as ToromarketError3 } from "@toromarket/sdk";
|
|
3692
3706
|
var ORDER_TOOLS = /* @__PURE__ */ new Set(["place_order", "cancel_order", "trade_crypto", "place_fund_order", "trade_fund_crypto", "cancel_fund_order"]);
|
|
3693
3707
|
var CHAT_TOOLS = /* @__PURE__ */ new Set(["post_market_comment", "post_fund_message", "react_to_chat"]);
|
|
3694
3708
|
var HEAVY_TOOLS = /* @__PURE__ */ new Set(["get_trading_context"]);
|
|
@@ -3735,7 +3749,7 @@ var RateLimiter = class {
|
|
|
3735
3749
|
const waitSec = Math.ceil((oldestInWindow + config.windowMs - now) / 1e3);
|
|
3736
3750
|
const proLimits = { orders: "100 orders/min", chat: "30 messages/min", reads: "300 reads/min" };
|
|
3737
3751
|
const upgradeHint = this.currentTier === "FREE" ? ` To increase your ${category} limit to ${proLimits[category] ?? "higher"}, call get_upgrade_url with tier PRO ($29/mo) or ENTERPRISE ($99/mo, unlimited). It will generate a payment link for your operator.` : this.currentTier === "PRO" ? ` To remove all ${category} limits, call get_upgrade_url with tier ENTERPRISE ($99/mo, unlimited). It will generate a payment link for your operator.` : "";
|
|
3738
|
-
throw new
|
|
3752
|
+
throw new ToromarketError3(
|
|
3739
3753
|
`Rate limit exceeded for ${category}. Limit: ${config.limit} per minute. Wait ${waitSec}s.${upgradeHint}`,
|
|
3740
3754
|
429,
|
|
3741
3755
|
"RATE_LIMITED"
|
|
@@ -3757,7 +3771,7 @@ function buildCategories(config) {
|
|
|
3757
3771
|
}
|
|
3758
3772
|
|
|
3759
3773
|
// src/middleware/error-budget.ts
|
|
3760
|
-
import { ToromarketError as
|
|
3774
|
+
import { ToromarketError as ToromarketError4 } from "@toromarket/sdk";
|
|
3761
3775
|
var ErrorBudget = class {
|
|
3762
3776
|
consecutiveErrors = 0;
|
|
3763
3777
|
cooldownUntil = 0;
|
|
@@ -3771,7 +3785,7 @@ var ErrorBudget = class {
|
|
|
3771
3785
|
const now = Date.now();
|
|
3772
3786
|
if (now < this.cooldownUntil) {
|
|
3773
3787
|
const waitSec = Math.ceil((this.cooldownUntil - now) / 1e3);
|
|
3774
|
-
throw new
|
|
3788
|
+
throw new ToromarketError4(
|
|
3775
3789
|
`Too many consecutive errors. Automatic backoff for ${waitSec}s.`,
|
|
3776
3790
|
429,
|
|
3777
3791
|
"ERROR_BUDGET_EXCEEDED"
|
|
@@ -3779,20 +3793,20 @@ var ErrorBudget = class {
|
|
|
3779
3793
|
}
|
|
3780
3794
|
}
|
|
3781
3795
|
async afterExecute(_toolName, _args, _result, error) {
|
|
3782
|
-
if (error instanceof
|
|
3783
|
-
const
|
|
3796
|
+
if (error instanceof ToromarketError4) {
|
|
3797
|
+
const EXCLUDED_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
3784
3798
|
"ERROR_BUDGET_EXCEEDED",
|
|
3785
3799
|
"RATE_LIMITED",
|
|
3786
3800
|
"REGISTRATION_THROTTLE",
|
|
3787
3801
|
"SPOOFING_DETECTED",
|
|
3788
3802
|
"SPENDING_LIMIT"
|
|
3789
3803
|
]);
|
|
3790
|
-
if (error.code &&
|
|
3804
|
+
if (error.code && EXCLUDED_ERROR_CODES.has(error.code)) {
|
|
3791
3805
|
return;
|
|
3792
3806
|
}
|
|
3793
3807
|
}
|
|
3794
3808
|
if (error) {
|
|
3795
|
-
const statusCode = error instanceof
|
|
3809
|
+
const statusCode = error instanceof ToromarketError4 ? error.statusCode : void 0;
|
|
3796
3810
|
if (statusCode !== void 0 && statusCode < 500) {
|
|
3797
3811
|
return;
|
|
3798
3812
|
}
|
|
@@ -3811,241 +3825,6 @@ var ErrorBudget = class {
|
|
|
3811
3825
|
}
|
|
3812
3826
|
};
|
|
3813
3827
|
|
|
3814
|
-
// src/middleware/spoofing.ts
|
|
3815
|
-
import { ToromarketError as ToromarketError6 } from "@toromarket/sdk";
|
|
3816
|
-
var SpoofingDetector = class {
|
|
3817
|
-
events = [];
|
|
3818
|
-
windowMs;
|
|
3819
|
-
maxCancelRatio;
|
|
3820
|
-
minActions;
|
|
3821
|
-
constructor(config = {}) {
|
|
3822
|
-
this.windowMs = config.windowMs ?? 3e4;
|
|
3823
|
-
this.maxCancelRatio = config.maxCancelRatio ?? 0.8;
|
|
3824
|
-
this.minActions = config.minActions ?? 5;
|
|
3825
|
-
}
|
|
3826
|
-
pruneExpired() {
|
|
3827
|
-
const cutoff = Date.now() - this.windowMs;
|
|
3828
|
-
while (this.events.length > 0 && this.events[0].time < cutoff) {
|
|
3829
|
-
this.events.shift();
|
|
3830
|
-
}
|
|
3831
|
-
}
|
|
3832
|
-
async beforeExecute(toolName, _args) {
|
|
3833
|
-
if (toolName !== "place_order" && toolName !== "place_fund_order") return;
|
|
3834
|
-
this.pruneExpired();
|
|
3835
|
-
if (this.events.length < this.minActions) return;
|
|
3836
|
-
const cancels = this.events.filter((e) => e.type === "cancel").length;
|
|
3837
|
-
const ratio = cancels / this.events.length;
|
|
3838
|
-
if (ratio > this.maxCancelRatio) {
|
|
3839
|
-
throw new ToromarketError6(
|
|
3840
|
-
`Suspicious trading pattern detected: ${Math.round(ratio * 100)}% of recent orders were cancelled. Slow down.`,
|
|
3841
|
-
403,
|
|
3842
|
-
"SPOOFING_DETECTED"
|
|
3843
|
-
);
|
|
3844
|
-
}
|
|
3845
|
-
}
|
|
3846
|
-
async afterExecute(toolName, _args, _result, error) {
|
|
3847
|
-
if (error) return;
|
|
3848
|
-
this.pruneExpired();
|
|
3849
|
-
if (toolName === "place_order" || toolName === "place_fund_order") {
|
|
3850
|
-
this.events.push({ type: "place", time: Date.now() });
|
|
3851
|
-
} else if (toolName === "cancel_order" || toolName === "cancel_fund_order") {
|
|
3852
|
-
this.events.push({ type: "cancel", time: Date.now() });
|
|
3853
|
-
}
|
|
3854
|
-
}
|
|
3855
|
-
};
|
|
3856
|
-
|
|
3857
|
-
// src/middleware/spending.ts
|
|
3858
|
-
import { ToromarketError as ToromarketError7 } from "@toromarket/sdk";
|
|
3859
|
-
var TRADE_TOOLS = /* @__PURE__ */ new Set(["place_order", "trade_crypto", "place_fund_order", "trade_fund_crypto"]);
|
|
3860
|
-
var SpendingGuardrails = class {
|
|
3861
|
-
startingBalance = null;
|
|
3862
|
-
blocked = false;
|
|
3863
|
-
maxLoss;
|
|
3864
|
-
constructor(maxSessionLoss2) {
|
|
3865
|
-
this.maxLoss = maxSessionLoss2 ?? 5e3;
|
|
3866
|
-
}
|
|
3867
|
-
async beforeExecute(toolName, _args) {
|
|
3868
|
-
if (!TRADE_TOOLS.has(toolName)) return;
|
|
3869
|
-
if (this.blocked) {
|
|
3870
|
-
throw new ToromarketError7(
|
|
3871
|
-
`Session spending limit reached (${this.maxLoss} TC). No further trades allowed. Read-only tools still work.`,
|
|
3872
|
-
403,
|
|
3873
|
-
"SPENDING_LIMIT"
|
|
3874
|
-
);
|
|
3875
|
-
}
|
|
3876
|
-
if (this.startingBalance === null) {
|
|
3877
|
-
throw new ToromarketError7(
|
|
3878
|
-
"Call get_balance or get_portfolio before your first trade so spending limits can be tracked.",
|
|
3879
|
-
400,
|
|
3880
|
-
"BALANCE_REQUIRED"
|
|
3881
|
-
);
|
|
3882
|
-
}
|
|
3883
|
-
}
|
|
3884
|
-
async afterExecute(toolName, _args, result, error) {
|
|
3885
|
-
if (error) return;
|
|
3886
|
-
if (this.maxLoss === null) return;
|
|
3887
|
-
if (this.startingBalance === null) {
|
|
3888
|
-
const bal = extractBalance(result);
|
|
3889
|
-
if (bal !== null) {
|
|
3890
|
-
this.startingBalance = bal;
|
|
3891
|
-
return;
|
|
3892
|
-
}
|
|
3893
|
-
}
|
|
3894
|
-
if (TRADE_TOOLS.has(toolName) && result) {
|
|
3895
|
-
const remaining = extractRemainingBalance(result);
|
|
3896
|
-
if (remaining !== null && this.startingBalance !== null) {
|
|
3897
|
-
const loss = this.startingBalance - remaining;
|
|
3898
|
-
if (loss >= this.maxLoss) {
|
|
3899
|
-
this.blocked = true;
|
|
3900
|
-
}
|
|
3901
|
-
}
|
|
3902
|
-
}
|
|
3903
|
-
}
|
|
3904
|
-
};
|
|
3905
|
-
function extractBalance(result) {
|
|
3906
|
-
if (typeof result !== "object" || result === null) return null;
|
|
3907
|
-
const r = result;
|
|
3908
|
-
if (typeof r.balance === "number") return r.balance;
|
|
3909
|
-
if (typeof r.totalValue === "number") return r.totalValue;
|
|
3910
|
-
return null;
|
|
3911
|
-
}
|
|
3912
|
-
function extractRemainingBalance(result) {
|
|
3913
|
-
if (typeof result !== "object" || result === null) return null;
|
|
3914
|
-
const r = result;
|
|
3915
|
-
if (typeof r.remainingBalance === "number") return r.remainingBalance;
|
|
3916
|
-
return null;
|
|
3917
|
-
}
|
|
3918
|
-
|
|
3919
|
-
// src/middleware/compliance-gate.ts
|
|
3920
|
-
var FINANCIAL_TOOLS = /* @__PURE__ */ new Set([
|
|
3921
|
-
"place_order",
|
|
3922
|
-
"cancel_order",
|
|
3923
|
-
"trade_crypto",
|
|
3924
|
-
"place_fund_order",
|
|
3925
|
-
"trade_fund_crypto",
|
|
3926
|
-
"cancel_fund_order",
|
|
3927
|
-
"create_escrow",
|
|
3928
|
-
"settle_escrow",
|
|
3929
|
-
"create_delegation",
|
|
3930
|
-
"topup_stake",
|
|
3931
|
-
"claim_challenge",
|
|
3932
|
-
"claim_quest",
|
|
3933
|
-
"claim_mystery_box"
|
|
3934
|
-
]);
|
|
3935
|
-
var ComplianceGate = class {
|
|
3936
|
-
constructor(config, logger) {
|
|
3937
|
-
this.config = config;
|
|
3938
|
-
this.logger = logger;
|
|
3939
|
-
}
|
|
3940
|
-
tradeTimestamps = [];
|
|
3941
|
-
sessionTcVolume = 0;
|
|
3942
|
-
region = null;
|
|
3943
|
-
flags = {
|
|
3944
|
-
velocityWarning: false,
|
|
3945
|
-
regionBlocked: false,
|
|
3946
|
-
largeTransaction: false
|
|
3947
|
-
};
|
|
3948
|
-
/**
|
|
3949
|
-
* Set the session's region for geographic restriction checks.
|
|
3950
|
-
* Call this when region info is available (e.g., from HTTP transport
|
|
3951
|
-
* X-Forwarded-For / CF-IPCountry headers, or operator-provided region).
|
|
3952
|
-
*/
|
|
3953
|
-
setRegion(region) {
|
|
3954
|
-
this.region = region.toUpperCase();
|
|
3955
|
-
if (this.config.blockedRegions.length > 0 && this.config.blockedRegions.includes(this.region)) {
|
|
3956
|
-
this.flags.regionBlocked = true;
|
|
3957
|
-
this.logger.warn({
|
|
3958
|
-
event: "compliance:region_blocked",
|
|
3959
|
-
region: this.region,
|
|
3960
|
-
blockedRegions: this.config.blockedRegions
|
|
3961
|
-
});
|
|
3962
|
-
}
|
|
3963
|
-
}
|
|
3964
|
-
async beforeExecute(toolName, args) {
|
|
3965
|
-
if (!this.config.enabled) return;
|
|
3966
|
-
if (!FINANCIAL_TOOLS.has(toolName)) return;
|
|
3967
|
-
const now = Date.now();
|
|
3968
|
-
this.tradeTimestamps.push(now);
|
|
3969
|
-
const oneMinAgo = now - 6e4;
|
|
3970
|
-
this.tradeTimestamps = this.tradeTimestamps.filter((t) => t > oneMinAgo);
|
|
3971
|
-
const amount = this.parseAmount(args);
|
|
3972
|
-
if (amount > 0) {
|
|
3973
|
-
this.sessionTcVolume += amount;
|
|
3974
|
-
}
|
|
3975
|
-
if (this.tradeTimestamps.length > this.config.velocityTradesPerMin) {
|
|
3976
|
-
this.flags.velocityWarning = true;
|
|
3977
|
-
this.logger.warn({
|
|
3978
|
-
event: "compliance:velocity_warning",
|
|
3979
|
-
tradesInLastMinute: this.tradeTimestamps.length,
|
|
3980
|
-
threshold: this.config.velocityTradesPerMin,
|
|
3981
|
-
tool: toolName
|
|
3982
|
-
});
|
|
3983
|
-
}
|
|
3984
|
-
if (this.sessionTcVolume > this.config.velocityTcPerHour) {
|
|
3985
|
-
this.flags.velocityWarning = true;
|
|
3986
|
-
this.logger.warn({
|
|
3987
|
-
event: "compliance:volume_warning",
|
|
3988
|
-
sessionVolume: this.sessionTcVolume,
|
|
3989
|
-
threshold: this.config.velocityTcPerHour,
|
|
3990
|
-
tool: toolName
|
|
3991
|
-
});
|
|
3992
|
-
}
|
|
3993
|
-
if (amount > 1e4) {
|
|
3994
|
-
this.flags.largeTransaction = true;
|
|
3995
|
-
this.logger.warn({
|
|
3996
|
-
event: "compliance:large_transaction",
|
|
3997
|
-
amount,
|
|
3998
|
-
tool: toolName
|
|
3999
|
-
});
|
|
4000
|
-
}
|
|
4001
|
-
if (this.flags.regionBlocked) {
|
|
4002
|
-
this.logger.warn({
|
|
4003
|
-
event: "compliance:region_blocked_trade_attempt",
|
|
4004
|
-
region: this.region,
|
|
4005
|
-
tool: toolName
|
|
4006
|
-
});
|
|
4007
|
-
}
|
|
4008
|
-
this.logger.info({
|
|
4009
|
-
event: "compliance:financial_tool",
|
|
4010
|
-
tool: toolName,
|
|
4011
|
-
amount: amount > 0 ? amount : void 0,
|
|
4012
|
-
sessionVolume: this.sessionTcVolume,
|
|
4013
|
-
tradesInLastMinute: this.tradeTimestamps.length,
|
|
4014
|
-
region: this.region,
|
|
4015
|
-
regionBlocked: this.flags.regionBlocked
|
|
4016
|
-
});
|
|
4017
|
-
}
|
|
4018
|
-
async afterExecute(_toolName, _args, _result, _error) {
|
|
4019
|
-
}
|
|
4020
|
-
getStatus() {
|
|
4021
|
-
const now = Date.now();
|
|
4022
|
-
const oneMinAgo = now - 6e4;
|
|
4023
|
-
const recentTrades = this.tradeTimestamps.filter((t) => t > oneMinAgo);
|
|
4024
|
-
return {
|
|
4025
|
-
enabled: this.config.enabled,
|
|
4026
|
-
flags: { ...this.flags },
|
|
4027
|
-
velocity: {
|
|
4028
|
-
tradesLastMinute: recentTrades.length,
|
|
4029
|
-
sessionVolume: this.sessionTcVolume
|
|
4030
|
-
},
|
|
4031
|
-
region: this.region,
|
|
4032
|
-
blockedRegions: this.config.blockedRegions
|
|
4033
|
-
};
|
|
4034
|
-
}
|
|
4035
|
-
parseAmount(args) {
|
|
4036
|
-
if (typeof args !== "object" || args === null) return 0;
|
|
4037
|
-
const a = args;
|
|
4038
|
-
if (typeof a.quantity === "number" && typeof a.price === "number") {
|
|
4039
|
-
return a.quantity * a.price;
|
|
4040
|
-
}
|
|
4041
|
-
if (typeof a.quantity === "number") return a.quantity;
|
|
4042
|
-
if (typeof a.amount === "number") return a.amount;
|
|
4043
|
-
if (typeof a.stake === "number") return a.stake;
|
|
4044
|
-
if (typeof a.initialStake === "number") return a.initialStake;
|
|
4045
|
-
return 0;
|
|
4046
|
-
}
|
|
4047
|
-
};
|
|
4048
|
-
|
|
4049
3828
|
// src/audit.ts
|
|
4050
3829
|
var AUDITED_TOOLS = /* @__PURE__ */ new Set([
|
|
4051
3830
|
"place_order",
|
|
@@ -4096,7 +3875,7 @@ function summarizeResult(result) {
|
|
|
4096
3875
|
}
|
|
4097
3876
|
|
|
4098
3877
|
// src/middleware/trace-collector.ts
|
|
4099
|
-
import { ToromarketError as
|
|
3878
|
+
import { ToromarketError as ToromarketError5 } from "@toromarket/sdk";
|
|
4100
3879
|
var STATE_CHANGING_TOOLS = /* @__PURE__ */ new Set([
|
|
4101
3880
|
"place_order",
|
|
4102
3881
|
"cancel_order",
|
|
@@ -4133,9 +3912,19 @@ var TraceCollector = class {
|
|
|
4133
3912
|
...signals !== void 0 ? { signals } : {}
|
|
4134
3913
|
};
|
|
4135
3914
|
}
|
|
4136
|
-
async beforeExecute(toolName) {
|
|
4137
|
-
if (STATE_CHANGING_TOOLS.has(toolName)
|
|
4138
|
-
|
|
3915
|
+
async beforeExecute(toolName, args) {
|
|
3916
|
+
if (!STATE_CHANGING_TOOLS.has(toolName)) return;
|
|
3917
|
+
if (!this.pendingReasoning && args && typeof args === "object") {
|
|
3918
|
+
const a = args;
|
|
3919
|
+
if (typeof a.reasoning === "string" && a.reasoning.length > 0) {
|
|
3920
|
+
this.setReasoning(
|
|
3921
|
+
a.reasoning,
|
|
3922
|
+
typeof a.confidence === "number" ? a.confidence : void 0
|
|
3923
|
+
);
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
if (!this.pendingReasoning) {
|
|
3927
|
+
throw new ToromarketError5(
|
|
4139
3928
|
"Call log_reasoning before trading. Every trade requires a reasoning explanation.",
|
|
4140
3929
|
400,
|
|
4141
3930
|
"REASONING_REQUIRED"
|
|
@@ -4484,7 +4273,7 @@ function normalizeOrder(raw) {
|
|
|
4484
4273
|
}
|
|
4485
4274
|
|
|
4486
4275
|
// src/metrics.ts
|
|
4487
|
-
var
|
|
4276
|
+
var TRADE_TOOLS = /* @__PURE__ */ new Set(["place_order", "cancel_order", "trade_crypto"]);
|
|
4488
4277
|
var MetricsCollector = class {
|
|
4489
4278
|
metrics;
|
|
4490
4279
|
logger;
|
|
@@ -4518,7 +4307,7 @@ var MetricsCollector = class {
|
|
|
4518
4307
|
metric.errors++;
|
|
4519
4308
|
this.metrics.totalErrors++;
|
|
4520
4309
|
}
|
|
4521
|
-
if (
|
|
4310
|
+
if (TRADE_TOOLS.has(toolName) && !error) {
|
|
4522
4311
|
this.metrics.totalTrades++;
|
|
4523
4312
|
}
|
|
4524
4313
|
}
|
|
@@ -4683,20 +4472,8 @@ function createServerFactory(options) {
|
|
|
4683
4472
|
const metrics = new MetricsCollector(logger);
|
|
4684
4473
|
const rateLimiter = new RateLimiter();
|
|
4685
4474
|
const traceCollector = new TraceCollector(client, logger, `session-${Date.now()}`);
|
|
4686
|
-
const complianceEnabled = !!process.env.TOROMARKET_COMPLIANCE_MODE;
|
|
4687
|
-
const blockedRegions = (process.env.TOROMARKET_BLOCKED_REGIONS ?? "").split(",").map((r) => r.trim().toUpperCase()).filter(Boolean);
|
|
4688
|
-
const velocityTradesPerMin = Number(process.env.TOROMARKET_VELOCITY_TRADES_PER_MIN) || 30;
|
|
4689
|
-
const velocityTcPerHour = Number(process.env.TOROMARKET_VELOCITY_TC_PER_HOUR) || 5e4;
|
|
4690
|
-
const complianceGate = new ComplianceGate(
|
|
4691
|
-
{ enabled: complianceEnabled, blockedRegions, velocityTradesPerMin, velocityTcPerHour },
|
|
4692
|
-
logger
|
|
4693
|
-
);
|
|
4694
4475
|
const middlewares = [
|
|
4695
|
-
new RegistrationThrottle(),
|
|
4696
4476
|
rateLimiter,
|
|
4697
|
-
new SpoofingDetector(),
|
|
4698
|
-
complianceGate,
|
|
4699
|
-
new SpendingGuardrails(options.maxSessionLoss),
|
|
4700
4477
|
new ErrorBudget(),
|
|
4701
4478
|
new AuditLogger(logger),
|
|
4702
4479
|
traceCollector
|
|
@@ -4706,7 +4483,6 @@ function createServerFactory(options) {
|
|
|
4706
4483
|
let currentTrustTier = null;
|
|
4707
4484
|
const executeOptions = {
|
|
4708
4485
|
traceCollector,
|
|
4709
|
-
complianceGate,
|
|
4710
4486
|
onTrustTier: (trustTier) => {
|
|
4711
4487
|
currentTrustTier = trustTier;
|
|
4712
4488
|
logger.info({ event: "trust_tier_applied", trustTier });
|
|
@@ -4747,8 +4523,7 @@ function createServerFactory(options) {
|
|
|
4747
4523
|
notificationService.stop();
|
|
4748
4524
|
notificationService = null;
|
|
4749
4525
|
}
|
|
4750
|
-
}
|
|
4751
|
-
setRegion: (region) => complianceGate.setRegion(region)
|
|
4526
|
+
}
|
|
4752
4527
|
};
|
|
4753
4528
|
};
|
|
4754
4529
|
return { logger, createMcpServer };
|
|
@@ -4835,10 +4610,6 @@ async function startHttpTransport(options) {
|
|
|
4835
4610
|
entry = { transport: transport2, server: createdServer, lastActivity: Date.now() };
|
|
4836
4611
|
sessions.set(newSessionId, entry);
|
|
4837
4612
|
logger.info({ event: "session_created", sessionId: newSessionId });
|
|
4838
|
-
const region = req.headers["cf-ipcountry"] ?? req.headers["x-vercel-ip-country"] ?? req.headers["x-country-code"];
|
|
4839
|
-
if (region && createdServer.setRegion) {
|
|
4840
|
-
createdServer.setRegion(region);
|
|
4841
|
-
}
|
|
4842
4613
|
transport2.onclose = () => {
|
|
4843
4614
|
createdServer.cleanup();
|
|
4844
4615
|
sessions.delete(newSessionId);
|
|
@@ -4933,8 +4704,6 @@ var baseUrl = readEnv("TOROMARKET_BASE_URL") ?? "https://api.toromarket.io";
|
|
|
4933
4704
|
var token = readEnv("TOROMARKET_TOKEN");
|
|
4934
4705
|
var apiKey = readEnv("TOROMARKET_API_KEY");
|
|
4935
4706
|
var logLevel = readEnv("TOROMARKET_LOG_LEVEL") ?? "info";
|
|
4936
|
-
var maxSessionLossStr = readEnv("TOROMARKET_MAX_SESSION_LOSS");
|
|
4937
|
-
var maxSessionLoss = maxSessionLossStr ? Number(maxSessionLossStr) : void 0;
|
|
4938
4707
|
var transport = readEnv("TOROMARKET_TRANSPORT") ?? "stdio";
|
|
4939
4708
|
var httpPort = Number(readEnv("TOROMARKET_HTTP_PORT") ?? "3001");
|
|
4940
4709
|
var agentSecret = readEnv("TOROMARKET_AGENT_SECRET");
|
|
@@ -4955,7 +4724,6 @@ var serverOptions = {
|
|
|
4955
4724
|
...token ? { token } : {},
|
|
4956
4725
|
...apiKey ? { apiKey } : {},
|
|
4957
4726
|
...agentSecret ? { agentSecret } : {},
|
|
4958
|
-
...maxSessionLoss ? { maxSessionLoss } : {},
|
|
4959
4727
|
...pollIntervalMs !== void 0 ? { pollIntervalMs } : {}
|
|
4960
4728
|
};
|
|
4961
4729
|
async function main() {
|