@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.cjs
CHANGED
|
@@ -27,7 +27,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
27
|
var import_server = require("@modelcontextprotocol/sdk/server/index.js");
|
|
28
28
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
29
29
|
var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
30
|
-
var
|
|
30
|
+
var import_sdk6 = require("@toromarket/sdk");
|
|
31
31
|
|
|
32
32
|
// src/tools/definitions.ts
|
|
33
33
|
var import_zod = require("zod");
|
|
@@ -81,7 +81,9 @@ var placeOrderSchema = import_zod.z.object({
|
|
|
81
81
|
side: import_zod.z.enum(["BUY", "SELL"]),
|
|
82
82
|
type: import_zod.z.enum(["LIMIT", "MARKET"]).default("LIMIT"),
|
|
83
83
|
price: import_zod.z.coerce.number().min(0).max(1).optional(),
|
|
84
|
-
quantity: import_zod.z.coerce.number().positive().max(1e5)
|
|
84
|
+
quantity: import_zod.z.coerce.number().positive().max(1e5),
|
|
85
|
+
reasoning: sanitized(1e3),
|
|
86
|
+
confidence: import_zod.z.coerce.number().min(0).max(1)
|
|
85
87
|
});
|
|
86
88
|
var cancelOrderSchema = import_zod.z.object({
|
|
87
89
|
marketId: id,
|
|
@@ -90,7 +92,9 @@ var cancelOrderSchema = import_zod.z.object({
|
|
|
90
92
|
var tradeCryptoSchema = import_zod.z.object({
|
|
91
93
|
symbol: sanitized(20),
|
|
92
94
|
side: import_zod.z.enum(["BUY", "SELL"]),
|
|
93
|
-
quantity: import_zod.z.coerce.number().positive().max(1e5)
|
|
95
|
+
quantity: import_zod.z.coerce.number().positive().max(1e5),
|
|
96
|
+
reasoning: sanitized(1e3),
|
|
97
|
+
confidence: import_zod.z.coerce.number().min(0).max(1)
|
|
94
98
|
});
|
|
95
99
|
var postMarketCommentSchema = import_zod.z.object({
|
|
96
100
|
marketId: id,
|
|
@@ -212,9 +216,11 @@ var PHASE1_TOOLS = [
|
|
|
212
216
|
type: "number",
|
|
213
217
|
description: "Price per share (0-1). Required for LIMIT orders. Ignored for MARKET orders."
|
|
214
218
|
},
|
|
215
|
-
quantity: { type: "number", description: "Share quantity" }
|
|
219
|
+
quantity: { type: "number", description: "Share quantity" },
|
|
220
|
+
reasoning: { type: "string", description: "Why you are making this trade \u2014 your thesis and key signals" },
|
|
221
|
+
confidence: { type: "number", description: "Confidence level 0-1 in this trade decision" }
|
|
216
222
|
},
|
|
217
|
-
required: ["marketId", "outcomeId", "side", "quantity"]
|
|
223
|
+
required: ["marketId", "outcomeId", "side", "quantity", "reasoning", "confidence"]
|
|
218
224
|
}
|
|
219
225
|
},
|
|
220
226
|
{
|
|
@@ -263,9 +269,11 @@ var PHASE1_TOOLS = [
|
|
|
263
269
|
properties: {
|
|
264
270
|
symbol: { type: "string", description: "Symbol like BTC" },
|
|
265
271
|
side: { type: "string", enum: ["BUY", "SELL"] },
|
|
266
|
-
quantity: { type: "number", description: "Order size" }
|
|
272
|
+
quantity: { type: "number", description: "Order size" },
|
|
273
|
+
reasoning: { type: "string", description: "Why you are making this trade \u2014 your thesis and key signals" },
|
|
274
|
+
confidence: { type: "number", description: "Confidence level 0-1 in this trade decision" }
|
|
267
275
|
},
|
|
268
|
-
required: ["symbol", "side", "quantity"]
|
|
276
|
+
required: ["symbol", "side", "quantity", "reasoning", "confidence"]
|
|
269
277
|
}
|
|
270
278
|
}
|
|
271
279
|
];
|
|
@@ -633,13 +641,17 @@ var placeFundOrderSchema = import_zod.z.object({
|
|
|
633
641
|
side: import_zod.z.enum(["BUY", "SELL"]),
|
|
634
642
|
type: import_zod.z.enum(["LIMIT", "MARKET"]).default("LIMIT"),
|
|
635
643
|
price: import_zod.z.coerce.number().min(0).max(1).optional(),
|
|
636
|
-
quantity: import_zod.z.coerce.number().positive().max(1e5)
|
|
644
|
+
quantity: import_zod.z.coerce.number().positive().max(1e5),
|
|
645
|
+
reasoning: sanitized(1e3),
|
|
646
|
+
confidence: import_zod.z.coerce.number().min(0).max(1)
|
|
637
647
|
});
|
|
638
648
|
var tradeFundCryptoSchema = import_zod.z.object({
|
|
639
649
|
fundId: id,
|
|
640
650
|
symbol: sanitized(20),
|
|
641
651
|
side: import_zod.z.enum(["BUY", "SELL"]),
|
|
642
|
-
quantity: import_zod.z.coerce.number().positive().max(1e5)
|
|
652
|
+
quantity: import_zod.z.coerce.number().positive().max(1e5),
|
|
653
|
+
reasoning: sanitized(1e3),
|
|
654
|
+
confidence: import_zod.z.coerce.number().min(0).max(1)
|
|
643
655
|
});
|
|
644
656
|
var FUND_TRADING_TOOLS = [
|
|
645
657
|
{
|
|
@@ -661,9 +673,11 @@ var FUND_TRADING_TOOLS = [
|
|
|
661
673
|
type: "number",
|
|
662
674
|
description: "Price per share (0-1). Required for LIMIT orders. Ignored for MARKET orders."
|
|
663
675
|
},
|
|
664
|
-
quantity: { type: "number", description: "Share quantity" }
|
|
676
|
+
quantity: { type: "number", description: "Share quantity" },
|
|
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" }
|
|
665
679
|
},
|
|
666
|
-
required: ["fundId", "marketId", "outcomeId", "side", "quantity"]
|
|
680
|
+
required: ["fundId", "marketId", "outcomeId", "side", "quantity", "reasoning", "confidence"]
|
|
667
681
|
}
|
|
668
682
|
},
|
|
669
683
|
{
|
|
@@ -675,9 +689,11 @@ var FUND_TRADING_TOOLS = [
|
|
|
675
689
|
fundId: { type: "string", description: "Your fund ID (from list_funds myFundId)" },
|
|
676
690
|
symbol: { type: "string", description: "Symbol like BTC" },
|
|
677
691
|
side: { type: "string", enum: ["BUY", "SELL"] },
|
|
678
|
-
quantity: { type: "number", description: "Order size" }
|
|
692
|
+
quantity: { type: "number", description: "Order size" },
|
|
693
|
+
reasoning: { type: "string", description: "Why you are making this trade \u2014 your thesis and key signals" },
|
|
694
|
+
confidence: { type: "number", description: "Confidence level 0-1 in this trade decision" }
|
|
679
695
|
},
|
|
680
|
-
required: ["fundId", "symbol", "side", "quantity"]
|
|
696
|
+
required: ["fundId", "symbol", "side", "quantity", "reasoning", "confidence"]
|
|
681
697
|
}
|
|
682
698
|
},
|
|
683
699
|
{
|
|
@@ -2504,7 +2520,9 @@ async function executeTool(client, baseUrl2, name, rawArgs, options) {
|
|
|
2504
2520
|
side: input.side,
|
|
2505
2521
|
type: input.type,
|
|
2506
2522
|
price: Number(price),
|
|
2507
|
-
quantity: Number(input.quantity)
|
|
2523
|
+
quantity: Number(input.quantity),
|
|
2524
|
+
reasoning: input.reasoning,
|
|
2525
|
+
confidence: input.confidence
|
|
2508
2526
|
});
|
|
2509
2527
|
const remainingBalance = await fetchRemainingBalance(client);
|
|
2510
2528
|
return { ...result, remainingBalance };
|
|
@@ -2531,7 +2549,13 @@ async function executeTool(client, baseUrl2, name, rawArgs, options) {
|
|
|
2531
2549
|
}
|
|
2532
2550
|
case "trade_crypto": {
|
|
2533
2551
|
const input = tradeCryptoSchema.parse(args);
|
|
2534
|
-
const result = await client.portfolio.trade(
|
|
2552
|
+
const result = await client.portfolio.trade({
|
|
2553
|
+
symbol: input.symbol,
|
|
2554
|
+
side: input.side,
|
|
2555
|
+
quantity: input.quantity,
|
|
2556
|
+
reasoning: input.reasoning,
|
|
2557
|
+
confidence: input.confidence
|
|
2558
|
+
});
|
|
2535
2559
|
const remainingBalance = await fetchRemainingBalance(client);
|
|
2536
2560
|
return { ...result, remainingBalance };
|
|
2537
2561
|
}
|
|
@@ -2725,7 +2749,9 @@ async function executeTool(client, baseUrl2, name, rawArgs, options) {
|
|
|
2725
2749
|
side: input.side,
|
|
2726
2750
|
type: input.type,
|
|
2727
2751
|
price,
|
|
2728
|
-
quantity: input.quantity
|
|
2752
|
+
quantity: input.quantity,
|
|
2753
|
+
reasoning: input.reasoning,
|
|
2754
|
+
confidence: input.confidence
|
|
2729
2755
|
});
|
|
2730
2756
|
const fundOrderBalance = await fetchRemainingBalance(client);
|
|
2731
2757
|
return { ...fundOrderResult, remainingBalance: fundOrderBalance };
|
|
@@ -2735,7 +2761,9 @@ async function executeTool(client, baseUrl2, name, rawArgs, options) {
|
|
|
2735
2761
|
const fundTradeResult = await client.funds.tradeCrypto(input.fundId, {
|
|
2736
2762
|
symbol: input.symbol,
|
|
2737
2763
|
side: input.side,
|
|
2738
|
-
quantity: input.quantity
|
|
2764
|
+
quantity: input.quantity,
|
|
2765
|
+
reasoning: input.reasoning,
|
|
2766
|
+
confidence: input.confidence
|
|
2739
2767
|
});
|
|
2740
2768
|
const fundTradeBalance = await fetchRemainingBalance(client);
|
|
2741
2769
|
return { ...fundTradeResult, remainingBalance: fundTradeBalance };
|
|
@@ -2971,17 +2999,11 @@ async function executeTool(client, baseUrl2, name, rawArgs, options) {
|
|
|
2971
2999
|
}
|
|
2972
3000
|
case "get_compliance_status": {
|
|
2973
3001
|
const me = await client.auth.me();
|
|
2974
|
-
const
|
|
2975
|
-
enabled: false,
|
|
2976
|
-
flags: { velocityWarning: false, regionBlocked: false, largeTransaction: false },
|
|
2977
|
-
velocity: { tradesLastMinute: 0, sessionVolume: 0 },
|
|
2978
|
-
region: null,
|
|
2979
|
-
blockedRegions: []
|
|
2980
|
-
};
|
|
3002
|
+
const compliance = await client.auth.complianceStatus();
|
|
2981
3003
|
return {
|
|
2982
3004
|
trustTier: me.trustTier,
|
|
2983
3005
|
trustScore: me.trustScore,
|
|
2984
|
-
compliance
|
|
3006
|
+
compliance
|
|
2985
3007
|
};
|
|
2986
3008
|
}
|
|
2987
3009
|
// Registry tools
|
|
@@ -3350,6 +3372,18 @@ function classifyError(error) {
|
|
|
3350
3372
|
if (error.code === "NETWORK_ERROR") {
|
|
3351
3373
|
return { error: error.message, statusCode: 0, retryable: true, recovery: "Toromarket API is unreachable. Check that the server is running." };
|
|
3352
3374
|
}
|
|
3375
|
+
if (error.code === "SPENDING_LIMIT") {
|
|
3376
|
+
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." };
|
|
3377
|
+
}
|
|
3378
|
+
if (error.code === "SPOOFING_DETECTED") {
|
|
3379
|
+
return { error: error.message, statusCode: 403, retryable: false, recovery: "Suspicious trading pattern detected (high cancel ratio). Slow down and wait 30 seconds." };
|
|
3380
|
+
}
|
|
3381
|
+
if (error.code === "REGISTRATION_THROTTLE") {
|
|
3382
|
+
return { error: error.message, statusCode: 403, retryable: false, recovery: "Registration limit reached for this IP. Use authenticate to log in to an existing account." };
|
|
3383
|
+
}
|
|
3384
|
+
if (error.code === "RATE_LIMITED") {
|
|
3385
|
+
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." };
|
|
3386
|
+
}
|
|
3353
3387
|
if (status >= 500) {
|
|
3354
3388
|
return { error: error.message, statusCode: status, retryable: true, recovery: "Server error. Try again in a few seconds." };
|
|
3355
3389
|
}
|
|
@@ -3683,28 +3717,8 @@ function createLogger(level = "info") {
|
|
|
3683
3717
|
};
|
|
3684
3718
|
}
|
|
3685
3719
|
|
|
3686
|
-
// src/middleware/registration.ts
|
|
3687
|
-
var import_sdk3 = require("@toromarket/sdk");
|
|
3688
|
-
var RegistrationThrottle = class {
|
|
3689
|
-
hasRegistered = false;
|
|
3690
|
-
async beforeExecute(toolName, _args) {
|
|
3691
|
-
if (toolName === "register_agent" && this.hasRegistered) {
|
|
3692
|
-
throw new import_sdk3.ToromarketError(
|
|
3693
|
-
"Only one account can be registered per session. Use authenticate to log in to an existing account.",
|
|
3694
|
-
403,
|
|
3695
|
-
"REGISTRATION_THROTTLE"
|
|
3696
|
-
);
|
|
3697
|
-
}
|
|
3698
|
-
}
|
|
3699
|
-
async afterExecute(toolName, _args, _result, error) {
|
|
3700
|
-
if (toolName === "register_agent" && !error) {
|
|
3701
|
-
this.hasRegistered = true;
|
|
3702
|
-
}
|
|
3703
|
-
}
|
|
3704
|
-
};
|
|
3705
|
-
|
|
3706
3720
|
// src/middleware/rate-limiter.ts
|
|
3707
|
-
var
|
|
3721
|
+
var import_sdk3 = require("@toromarket/sdk");
|
|
3708
3722
|
var ORDER_TOOLS = /* @__PURE__ */ new Set(["place_order", "cancel_order", "trade_crypto", "place_fund_order", "trade_fund_crypto", "cancel_fund_order"]);
|
|
3709
3723
|
var CHAT_TOOLS = /* @__PURE__ */ new Set(["post_market_comment", "post_fund_message", "react_to_chat"]);
|
|
3710
3724
|
var HEAVY_TOOLS = /* @__PURE__ */ new Set(["get_trading_context"]);
|
|
@@ -3751,7 +3765,7 @@ var RateLimiter = class {
|
|
|
3751
3765
|
const waitSec = Math.ceil((oldestInWindow + config.windowMs - now) / 1e3);
|
|
3752
3766
|
const proLimits = { orders: "100 orders/min", chat: "30 messages/min", reads: "300 reads/min" };
|
|
3753
3767
|
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.` : "";
|
|
3754
|
-
throw new
|
|
3768
|
+
throw new import_sdk3.ToromarketError(
|
|
3755
3769
|
`Rate limit exceeded for ${category}. Limit: ${config.limit} per minute. Wait ${waitSec}s.${upgradeHint}`,
|
|
3756
3770
|
429,
|
|
3757
3771
|
"RATE_LIMITED"
|
|
@@ -3773,7 +3787,7 @@ function buildCategories(config) {
|
|
|
3773
3787
|
}
|
|
3774
3788
|
|
|
3775
3789
|
// src/middleware/error-budget.ts
|
|
3776
|
-
var
|
|
3790
|
+
var import_sdk4 = require("@toromarket/sdk");
|
|
3777
3791
|
var ErrorBudget = class {
|
|
3778
3792
|
consecutiveErrors = 0;
|
|
3779
3793
|
cooldownUntil = 0;
|
|
@@ -3787,7 +3801,7 @@ var ErrorBudget = class {
|
|
|
3787
3801
|
const now = Date.now();
|
|
3788
3802
|
if (now < this.cooldownUntil) {
|
|
3789
3803
|
const waitSec = Math.ceil((this.cooldownUntil - now) / 1e3);
|
|
3790
|
-
throw new
|
|
3804
|
+
throw new import_sdk4.ToromarketError(
|
|
3791
3805
|
`Too many consecutive errors. Automatic backoff for ${waitSec}s.`,
|
|
3792
3806
|
429,
|
|
3793
3807
|
"ERROR_BUDGET_EXCEEDED"
|
|
@@ -3795,20 +3809,20 @@ var ErrorBudget = class {
|
|
|
3795
3809
|
}
|
|
3796
3810
|
}
|
|
3797
3811
|
async afterExecute(_toolName, _args, _result, error) {
|
|
3798
|
-
if (error instanceof
|
|
3799
|
-
const
|
|
3812
|
+
if (error instanceof import_sdk4.ToromarketError) {
|
|
3813
|
+
const EXCLUDED_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
3800
3814
|
"ERROR_BUDGET_EXCEEDED",
|
|
3801
3815
|
"RATE_LIMITED",
|
|
3802
3816
|
"REGISTRATION_THROTTLE",
|
|
3803
3817
|
"SPOOFING_DETECTED",
|
|
3804
3818
|
"SPENDING_LIMIT"
|
|
3805
3819
|
]);
|
|
3806
|
-
if (error.code &&
|
|
3820
|
+
if (error.code && EXCLUDED_ERROR_CODES.has(error.code)) {
|
|
3807
3821
|
return;
|
|
3808
3822
|
}
|
|
3809
3823
|
}
|
|
3810
3824
|
if (error) {
|
|
3811
|
-
const statusCode = error instanceof
|
|
3825
|
+
const statusCode = error instanceof import_sdk4.ToromarketError ? error.statusCode : void 0;
|
|
3812
3826
|
if (statusCode !== void 0 && statusCode < 500) {
|
|
3813
3827
|
return;
|
|
3814
3828
|
}
|
|
@@ -3827,241 +3841,6 @@ var ErrorBudget = class {
|
|
|
3827
3841
|
}
|
|
3828
3842
|
};
|
|
3829
3843
|
|
|
3830
|
-
// src/middleware/spoofing.ts
|
|
3831
|
-
var import_sdk6 = require("@toromarket/sdk");
|
|
3832
|
-
var SpoofingDetector = class {
|
|
3833
|
-
events = [];
|
|
3834
|
-
windowMs;
|
|
3835
|
-
maxCancelRatio;
|
|
3836
|
-
minActions;
|
|
3837
|
-
constructor(config = {}) {
|
|
3838
|
-
this.windowMs = config.windowMs ?? 3e4;
|
|
3839
|
-
this.maxCancelRatio = config.maxCancelRatio ?? 0.8;
|
|
3840
|
-
this.minActions = config.minActions ?? 5;
|
|
3841
|
-
}
|
|
3842
|
-
pruneExpired() {
|
|
3843
|
-
const cutoff = Date.now() - this.windowMs;
|
|
3844
|
-
while (this.events.length > 0 && this.events[0].time < cutoff) {
|
|
3845
|
-
this.events.shift();
|
|
3846
|
-
}
|
|
3847
|
-
}
|
|
3848
|
-
async beforeExecute(toolName, _args) {
|
|
3849
|
-
if (toolName !== "place_order" && toolName !== "place_fund_order") return;
|
|
3850
|
-
this.pruneExpired();
|
|
3851
|
-
if (this.events.length < this.minActions) return;
|
|
3852
|
-
const cancels = this.events.filter((e) => e.type === "cancel").length;
|
|
3853
|
-
const ratio = cancels / this.events.length;
|
|
3854
|
-
if (ratio > this.maxCancelRatio) {
|
|
3855
|
-
throw new import_sdk6.ToromarketError(
|
|
3856
|
-
`Suspicious trading pattern detected: ${Math.round(ratio * 100)}% of recent orders were cancelled. Slow down.`,
|
|
3857
|
-
403,
|
|
3858
|
-
"SPOOFING_DETECTED"
|
|
3859
|
-
);
|
|
3860
|
-
}
|
|
3861
|
-
}
|
|
3862
|
-
async afterExecute(toolName, _args, _result, error) {
|
|
3863
|
-
if (error) return;
|
|
3864
|
-
this.pruneExpired();
|
|
3865
|
-
if (toolName === "place_order" || toolName === "place_fund_order") {
|
|
3866
|
-
this.events.push({ type: "place", time: Date.now() });
|
|
3867
|
-
} else if (toolName === "cancel_order" || toolName === "cancel_fund_order") {
|
|
3868
|
-
this.events.push({ type: "cancel", time: Date.now() });
|
|
3869
|
-
}
|
|
3870
|
-
}
|
|
3871
|
-
};
|
|
3872
|
-
|
|
3873
|
-
// src/middleware/spending.ts
|
|
3874
|
-
var import_sdk7 = require("@toromarket/sdk");
|
|
3875
|
-
var TRADE_TOOLS = /* @__PURE__ */ new Set(["place_order", "trade_crypto", "place_fund_order", "trade_fund_crypto"]);
|
|
3876
|
-
var SpendingGuardrails = class {
|
|
3877
|
-
startingBalance = null;
|
|
3878
|
-
blocked = false;
|
|
3879
|
-
maxLoss;
|
|
3880
|
-
constructor(maxSessionLoss2) {
|
|
3881
|
-
this.maxLoss = maxSessionLoss2 ?? 5e3;
|
|
3882
|
-
}
|
|
3883
|
-
async beforeExecute(toolName, _args) {
|
|
3884
|
-
if (!TRADE_TOOLS.has(toolName)) return;
|
|
3885
|
-
if (this.blocked) {
|
|
3886
|
-
throw new import_sdk7.ToromarketError(
|
|
3887
|
-
`Session spending limit reached (${this.maxLoss} TC). No further trades allowed. Read-only tools still work.`,
|
|
3888
|
-
403,
|
|
3889
|
-
"SPENDING_LIMIT"
|
|
3890
|
-
);
|
|
3891
|
-
}
|
|
3892
|
-
if (this.startingBalance === null) {
|
|
3893
|
-
throw new import_sdk7.ToromarketError(
|
|
3894
|
-
"Call get_balance or get_portfolio before your first trade so spending limits can be tracked.",
|
|
3895
|
-
400,
|
|
3896
|
-
"BALANCE_REQUIRED"
|
|
3897
|
-
);
|
|
3898
|
-
}
|
|
3899
|
-
}
|
|
3900
|
-
async afterExecute(toolName, _args, result, error) {
|
|
3901
|
-
if (error) return;
|
|
3902
|
-
if (this.maxLoss === null) return;
|
|
3903
|
-
if (this.startingBalance === null) {
|
|
3904
|
-
const bal = extractBalance(result);
|
|
3905
|
-
if (bal !== null) {
|
|
3906
|
-
this.startingBalance = bal;
|
|
3907
|
-
return;
|
|
3908
|
-
}
|
|
3909
|
-
}
|
|
3910
|
-
if (TRADE_TOOLS.has(toolName) && result) {
|
|
3911
|
-
const remaining = extractRemainingBalance(result);
|
|
3912
|
-
if (remaining !== null && this.startingBalance !== null) {
|
|
3913
|
-
const loss = this.startingBalance - remaining;
|
|
3914
|
-
if (loss >= this.maxLoss) {
|
|
3915
|
-
this.blocked = true;
|
|
3916
|
-
}
|
|
3917
|
-
}
|
|
3918
|
-
}
|
|
3919
|
-
}
|
|
3920
|
-
};
|
|
3921
|
-
function extractBalance(result) {
|
|
3922
|
-
if (typeof result !== "object" || result === null) return null;
|
|
3923
|
-
const r = result;
|
|
3924
|
-
if (typeof r.balance === "number") return r.balance;
|
|
3925
|
-
if (typeof r.totalValue === "number") return r.totalValue;
|
|
3926
|
-
return null;
|
|
3927
|
-
}
|
|
3928
|
-
function extractRemainingBalance(result) {
|
|
3929
|
-
if (typeof result !== "object" || result === null) return null;
|
|
3930
|
-
const r = result;
|
|
3931
|
-
if (typeof r.remainingBalance === "number") return r.remainingBalance;
|
|
3932
|
-
return null;
|
|
3933
|
-
}
|
|
3934
|
-
|
|
3935
|
-
// src/middleware/compliance-gate.ts
|
|
3936
|
-
var FINANCIAL_TOOLS = /* @__PURE__ */ new Set([
|
|
3937
|
-
"place_order",
|
|
3938
|
-
"cancel_order",
|
|
3939
|
-
"trade_crypto",
|
|
3940
|
-
"place_fund_order",
|
|
3941
|
-
"trade_fund_crypto",
|
|
3942
|
-
"cancel_fund_order",
|
|
3943
|
-
"create_escrow",
|
|
3944
|
-
"settle_escrow",
|
|
3945
|
-
"create_delegation",
|
|
3946
|
-
"topup_stake",
|
|
3947
|
-
"claim_challenge",
|
|
3948
|
-
"claim_quest",
|
|
3949
|
-
"claim_mystery_box"
|
|
3950
|
-
]);
|
|
3951
|
-
var ComplianceGate = class {
|
|
3952
|
-
constructor(config, logger) {
|
|
3953
|
-
this.config = config;
|
|
3954
|
-
this.logger = logger;
|
|
3955
|
-
}
|
|
3956
|
-
tradeTimestamps = [];
|
|
3957
|
-
sessionTcVolume = 0;
|
|
3958
|
-
region = null;
|
|
3959
|
-
flags = {
|
|
3960
|
-
velocityWarning: false,
|
|
3961
|
-
regionBlocked: false,
|
|
3962
|
-
largeTransaction: false
|
|
3963
|
-
};
|
|
3964
|
-
/**
|
|
3965
|
-
* Set the session's region for geographic restriction checks.
|
|
3966
|
-
* Call this when region info is available (e.g., from HTTP transport
|
|
3967
|
-
* X-Forwarded-For / CF-IPCountry headers, or operator-provided region).
|
|
3968
|
-
*/
|
|
3969
|
-
setRegion(region) {
|
|
3970
|
-
this.region = region.toUpperCase();
|
|
3971
|
-
if (this.config.blockedRegions.length > 0 && this.config.blockedRegions.includes(this.region)) {
|
|
3972
|
-
this.flags.regionBlocked = true;
|
|
3973
|
-
this.logger.warn({
|
|
3974
|
-
event: "compliance:region_blocked",
|
|
3975
|
-
region: this.region,
|
|
3976
|
-
blockedRegions: this.config.blockedRegions
|
|
3977
|
-
});
|
|
3978
|
-
}
|
|
3979
|
-
}
|
|
3980
|
-
async beforeExecute(toolName, args) {
|
|
3981
|
-
if (!this.config.enabled) return;
|
|
3982
|
-
if (!FINANCIAL_TOOLS.has(toolName)) return;
|
|
3983
|
-
const now = Date.now();
|
|
3984
|
-
this.tradeTimestamps.push(now);
|
|
3985
|
-
const oneMinAgo = now - 6e4;
|
|
3986
|
-
this.tradeTimestamps = this.tradeTimestamps.filter((t) => t > oneMinAgo);
|
|
3987
|
-
const amount = this.parseAmount(args);
|
|
3988
|
-
if (amount > 0) {
|
|
3989
|
-
this.sessionTcVolume += amount;
|
|
3990
|
-
}
|
|
3991
|
-
if (this.tradeTimestamps.length > this.config.velocityTradesPerMin) {
|
|
3992
|
-
this.flags.velocityWarning = true;
|
|
3993
|
-
this.logger.warn({
|
|
3994
|
-
event: "compliance:velocity_warning",
|
|
3995
|
-
tradesInLastMinute: this.tradeTimestamps.length,
|
|
3996
|
-
threshold: this.config.velocityTradesPerMin,
|
|
3997
|
-
tool: toolName
|
|
3998
|
-
});
|
|
3999
|
-
}
|
|
4000
|
-
if (this.sessionTcVolume > this.config.velocityTcPerHour) {
|
|
4001
|
-
this.flags.velocityWarning = true;
|
|
4002
|
-
this.logger.warn({
|
|
4003
|
-
event: "compliance:volume_warning",
|
|
4004
|
-
sessionVolume: this.sessionTcVolume,
|
|
4005
|
-
threshold: this.config.velocityTcPerHour,
|
|
4006
|
-
tool: toolName
|
|
4007
|
-
});
|
|
4008
|
-
}
|
|
4009
|
-
if (amount > 1e4) {
|
|
4010
|
-
this.flags.largeTransaction = true;
|
|
4011
|
-
this.logger.warn({
|
|
4012
|
-
event: "compliance:large_transaction",
|
|
4013
|
-
amount,
|
|
4014
|
-
tool: toolName
|
|
4015
|
-
});
|
|
4016
|
-
}
|
|
4017
|
-
if (this.flags.regionBlocked) {
|
|
4018
|
-
this.logger.warn({
|
|
4019
|
-
event: "compliance:region_blocked_trade_attempt",
|
|
4020
|
-
region: this.region,
|
|
4021
|
-
tool: toolName
|
|
4022
|
-
});
|
|
4023
|
-
}
|
|
4024
|
-
this.logger.info({
|
|
4025
|
-
event: "compliance:financial_tool",
|
|
4026
|
-
tool: toolName,
|
|
4027
|
-
amount: amount > 0 ? amount : void 0,
|
|
4028
|
-
sessionVolume: this.sessionTcVolume,
|
|
4029
|
-
tradesInLastMinute: this.tradeTimestamps.length,
|
|
4030
|
-
region: this.region,
|
|
4031
|
-
regionBlocked: this.flags.regionBlocked
|
|
4032
|
-
});
|
|
4033
|
-
}
|
|
4034
|
-
async afterExecute(_toolName, _args, _result, _error) {
|
|
4035
|
-
}
|
|
4036
|
-
getStatus() {
|
|
4037
|
-
const now = Date.now();
|
|
4038
|
-
const oneMinAgo = now - 6e4;
|
|
4039
|
-
const recentTrades = this.tradeTimestamps.filter((t) => t > oneMinAgo);
|
|
4040
|
-
return {
|
|
4041
|
-
enabled: this.config.enabled,
|
|
4042
|
-
flags: { ...this.flags },
|
|
4043
|
-
velocity: {
|
|
4044
|
-
tradesLastMinute: recentTrades.length,
|
|
4045
|
-
sessionVolume: this.sessionTcVolume
|
|
4046
|
-
},
|
|
4047
|
-
region: this.region,
|
|
4048
|
-
blockedRegions: this.config.blockedRegions
|
|
4049
|
-
};
|
|
4050
|
-
}
|
|
4051
|
-
parseAmount(args) {
|
|
4052
|
-
if (typeof args !== "object" || args === null) return 0;
|
|
4053
|
-
const a = args;
|
|
4054
|
-
if (typeof a.quantity === "number" && typeof a.price === "number") {
|
|
4055
|
-
return a.quantity * a.price;
|
|
4056
|
-
}
|
|
4057
|
-
if (typeof a.quantity === "number") return a.quantity;
|
|
4058
|
-
if (typeof a.amount === "number") return a.amount;
|
|
4059
|
-
if (typeof a.stake === "number") return a.stake;
|
|
4060
|
-
if (typeof a.initialStake === "number") return a.initialStake;
|
|
4061
|
-
return 0;
|
|
4062
|
-
}
|
|
4063
|
-
};
|
|
4064
|
-
|
|
4065
3844
|
// src/audit.ts
|
|
4066
3845
|
var AUDITED_TOOLS = /* @__PURE__ */ new Set([
|
|
4067
3846
|
"place_order",
|
|
@@ -4112,7 +3891,7 @@ function summarizeResult(result) {
|
|
|
4112
3891
|
}
|
|
4113
3892
|
|
|
4114
3893
|
// src/middleware/trace-collector.ts
|
|
4115
|
-
var
|
|
3894
|
+
var import_sdk5 = require("@toromarket/sdk");
|
|
4116
3895
|
var STATE_CHANGING_TOOLS = /* @__PURE__ */ new Set([
|
|
4117
3896
|
"place_order",
|
|
4118
3897
|
"cancel_order",
|
|
@@ -4149,9 +3928,19 @@ var TraceCollector = class {
|
|
|
4149
3928
|
...signals !== void 0 ? { signals } : {}
|
|
4150
3929
|
};
|
|
4151
3930
|
}
|
|
4152
|
-
async beforeExecute(toolName) {
|
|
4153
|
-
if (STATE_CHANGING_TOOLS.has(toolName)
|
|
4154
|
-
|
|
3931
|
+
async beforeExecute(toolName, args) {
|
|
3932
|
+
if (!STATE_CHANGING_TOOLS.has(toolName)) return;
|
|
3933
|
+
if (!this.pendingReasoning && args && typeof args === "object") {
|
|
3934
|
+
const a = args;
|
|
3935
|
+
if (typeof a.reasoning === "string" && a.reasoning.length > 0) {
|
|
3936
|
+
this.setReasoning(
|
|
3937
|
+
a.reasoning,
|
|
3938
|
+
typeof a.confidence === "number" ? a.confidence : void 0
|
|
3939
|
+
);
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
if (!this.pendingReasoning) {
|
|
3943
|
+
throw new import_sdk5.ToromarketError(
|
|
4155
3944
|
"Call log_reasoning before trading. Every trade requires a reasoning explanation.",
|
|
4156
3945
|
400,
|
|
4157
3946
|
"REASONING_REQUIRED"
|
|
@@ -4500,7 +4289,7 @@ function normalizeOrder(raw) {
|
|
|
4500
4289
|
}
|
|
4501
4290
|
|
|
4502
4291
|
// src/metrics.ts
|
|
4503
|
-
var
|
|
4292
|
+
var TRADE_TOOLS = /* @__PURE__ */ new Set(["place_order", "cancel_order", "trade_crypto"]);
|
|
4504
4293
|
var MetricsCollector = class {
|
|
4505
4294
|
metrics;
|
|
4506
4295
|
logger;
|
|
@@ -4534,7 +4323,7 @@ var MetricsCollector = class {
|
|
|
4534
4323
|
metric.errors++;
|
|
4535
4324
|
this.metrics.totalErrors++;
|
|
4536
4325
|
}
|
|
4537
|
-
if (
|
|
4326
|
+
if (TRADE_TOOLS.has(toolName) && !error) {
|
|
4538
4327
|
this.metrics.totalTrades++;
|
|
4539
4328
|
}
|
|
4540
4329
|
}
|
|
@@ -4561,7 +4350,7 @@ var MetricsCollector = class {
|
|
|
4561
4350
|
|
|
4562
4351
|
// src/server.ts
|
|
4563
4352
|
function createToromarketClient(options) {
|
|
4564
|
-
return new
|
|
4353
|
+
return new import_sdk6.ToromarketClient({
|
|
4565
4354
|
baseUrl: options.baseUrl,
|
|
4566
4355
|
clientId: "mcp-server/0.2.0",
|
|
4567
4356
|
...options.token ? { token: options.token } : {},
|
|
@@ -4699,20 +4488,8 @@ function createServerFactory(options) {
|
|
|
4699
4488
|
const metrics = new MetricsCollector(logger);
|
|
4700
4489
|
const rateLimiter = new RateLimiter();
|
|
4701
4490
|
const traceCollector = new TraceCollector(client, logger, `session-${Date.now()}`);
|
|
4702
|
-
const complianceEnabled = !!process.env.TOROMARKET_COMPLIANCE_MODE;
|
|
4703
|
-
const blockedRegions = (process.env.TOROMARKET_BLOCKED_REGIONS ?? "").split(",").map((r) => r.trim().toUpperCase()).filter(Boolean);
|
|
4704
|
-
const velocityTradesPerMin = Number(process.env.TOROMARKET_VELOCITY_TRADES_PER_MIN) || 30;
|
|
4705
|
-
const velocityTcPerHour = Number(process.env.TOROMARKET_VELOCITY_TC_PER_HOUR) || 5e4;
|
|
4706
|
-
const complianceGate = new ComplianceGate(
|
|
4707
|
-
{ enabled: complianceEnabled, blockedRegions, velocityTradesPerMin, velocityTcPerHour },
|
|
4708
|
-
logger
|
|
4709
|
-
);
|
|
4710
4491
|
const middlewares = [
|
|
4711
|
-
new RegistrationThrottle(),
|
|
4712
4492
|
rateLimiter,
|
|
4713
|
-
new SpoofingDetector(),
|
|
4714
|
-
complianceGate,
|
|
4715
|
-
new SpendingGuardrails(options.maxSessionLoss),
|
|
4716
4493
|
new ErrorBudget(),
|
|
4717
4494
|
new AuditLogger(logger),
|
|
4718
4495
|
traceCollector
|
|
@@ -4722,7 +4499,6 @@ function createServerFactory(options) {
|
|
|
4722
4499
|
let currentTrustTier = null;
|
|
4723
4500
|
const executeOptions = {
|
|
4724
4501
|
traceCollector,
|
|
4725
|
-
complianceGate,
|
|
4726
4502
|
onTrustTier: (trustTier) => {
|
|
4727
4503
|
currentTrustTier = trustTier;
|
|
4728
4504
|
logger.info({ event: "trust_tier_applied", trustTier });
|
|
@@ -4763,8 +4539,7 @@ function createServerFactory(options) {
|
|
|
4763
4539
|
notificationService.stop();
|
|
4764
4540
|
notificationService = null;
|
|
4765
4541
|
}
|
|
4766
|
-
}
|
|
4767
|
-
setRegion: (region) => complianceGate.setRegion(region)
|
|
4542
|
+
}
|
|
4768
4543
|
};
|
|
4769
4544
|
};
|
|
4770
4545
|
return { logger, createMcpServer };
|
|
@@ -4851,10 +4626,6 @@ async function startHttpTransport(options) {
|
|
|
4851
4626
|
entry = { transport: transport2, server: createdServer, lastActivity: Date.now() };
|
|
4852
4627
|
sessions.set(newSessionId, entry);
|
|
4853
4628
|
logger.info({ event: "session_created", sessionId: newSessionId });
|
|
4854
|
-
const region = req.headers["cf-ipcountry"] ?? req.headers["x-vercel-ip-country"] ?? req.headers["x-country-code"];
|
|
4855
|
-
if (region && createdServer.setRegion) {
|
|
4856
|
-
createdServer.setRegion(region);
|
|
4857
|
-
}
|
|
4858
4629
|
transport2.onclose = () => {
|
|
4859
4630
|
createdServer.cleanup();
|
|
4860
4631
|
sessions.delete(newSessionId);
|
|
@@ -4949,8 +4720,6 @@ var baseUrl = readEnv("TOROMARKET_BASE_URL") ?? "https://api.toromarket.io";
|
|
|
4949
4720
|
var token = readEnv("TOROMARKET_TOKEN");
|
|
4950
4721
|
var apiKey = readEnv("TOROMARKET_API_KEY");
|
|
4951
4722
|
var logLevel = readEnv("TOROMARKET_LOG_LEVEL") ?? "info";
|
|
4952
|
-
var maxSessionLossStr = readEnv("TOROMARKET_MAX_SESSION_LOSS");
|
|
4953
|
-
var maxSessionLoss = maxSessionLossStr ? Number(maxSessionLossStr) : void 0;
|
|
4954
4723
|
var transport = readEnv("TOROMARKET_TRANSPORT") ?? "stdio";
|
|
4955
4724
|
var httpPort = Number(readEnv("TOROMARKET_HTTP_PORT") ?? "3001");
|
|
4956
4725
|
var agentSecret = readEnv("TOROMARKET_AGENT_SECRET");
|
|
@@ -4971,7 +4740,6 @@ var serverOptions = {
|
|
|
4971
4740
|
...token ? { token } : {},
|
|
4972
4741
|
...apiKey ? { apiKey } : {},
|
|
4973
4742
|
...agentSecret ? { agentSecret } : {},
|
|
4974
|
-
...maxSessionLoss ? { maxSessionLoss } : {},
|
|
4975
4743
|
...pollIntervalMs !== void 0 ? { pollIntervalMs } : {}
|
|
4976
4744
|
};
|
|
4977
4745
|
async function main() {
|