shll-skills 5.2.1 → 5.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp.js CHANGED
@@ -613,7 +613,12 @@ var WBNB_ABI = [
613
613
  ];
614
614
  var SPENDING_LIMIT_ABI = [
615
615
  { type: "function", name: "setLimits", inputs: [{ name: "instanceId", type: "uint256" }, { name: "maxPerTx", type: "uint256" }, { name: "maxPerDay", type: "uint256" }, { name: "maxSlippageBps", type: "uint256" }], outputs: [], stateMutability: "nonpayable" },
616
- { type: "function", name: "instanceLimits", inputs: [{ name: "instanceId", type: "uint256" }], outputs: [{ name: "maxPerTx", type: "uint256" }, { name: "maxPerDay", type: "uint256" }, { name: "maxSlippageBps", type: "uint256" }], stateMutability: "view" }
616
+ { type: "function", name: "instanceLimits", inputs: [{ name: "instanceId", type: "uint256" }], outputs: [{ name: "maxPerTx", type: "uint256" }, { name: "maxPerDay", type: "uint256" }, { name: "maxSlippageBps", type: "uint256" }], stateMutability: "view" },
617
+ { type: "function", name: "tokenRestrictionEnabled", inputs: [{ name: "instanceId", type: "uint256" }], outputs: [{ name: "", type: "bool" }], stateMutability: "view" },
618
+ { type: "function", name: "getTokenList", inputs: [{ name: "instanceId", type: "uint256" }], outputs: [{ name: "", type: "address[]" }], stateMutability: "view" },
619
+ { type: "function", name: "addToken", inputs: [{ name: "instanceId", type: "uint256" }, { name: "token", type: "address" }], outputs: [], stateMutability: "nonpayable" },
620
+ { type: "function", name: "removeToken", inputs: [{ name: "instanceId", type: "uint256" }, { name: "token", type: "address" }], outputs: [], stateMutability: "nonpayable" },
621
+ { type: "function", name: "setTokenRestriction", inputs: [{ name: "instanceId", type: "uint256" }, { name: "enabled", type: "bool" }], outputs: [], stateMutability: "nonpayable" }
617
622
  ];
618
623
  var COOLDOWN_ABI = [
619
624
  { type: "function", name: "setCooldown", inputs: [{ name: "instanceId", type: "uint256" }, { name: "seconds_", type: "uint256" }], outputs: [], stateMutability: "nonpayable" },
@@ -683,9 +688,28 @@ async function checkAgentExpiry(tokenId) {
683
688
  }
684
689
  return { expired: false };
685
690
  }
691
+ function policyRejectionHelp(reason, tokenId) {
692
+ const consoleUrl = `https://shll.run/agent/0xE98DCdbf370D7b52c9A2b88F79bEF514A5375a2b/${tokenId}/console/safety`;
693
+ const r = reason ?? "";
694
+ if (r.includes("Approve spender not allowed"))
695
+ return { explanation: "The DEX router address is not in the approved spender whitelist. This is a platform-level security setting.", action: "Contact the Agent owner to approve this router, or use a different DEX.", consoleUrl };
696
+ if (r.includes("Target not allowed"))
697
+ return { explanation: "The contract address is not in the DeFi target whitelist.", action: "Enable the corresponding DeFi Pack in Console > Safety, or contact the Agent owner.", consoleUrl };
698
+ if (r.includes("Token not in whitelist"))
699
+ return { explanation: "Token restriction is ON and this token is not whitelisted.", action: `Add the token to the whitelist or disable token restriction at: ${consoleUrl}`, consoleUrl };
700
+ if (r.includes("Exceeds per-tx limit"))
701
+ return { explanation: "Transaction value exceeds the per-transaction spending limit.", action: `Reduce the amount, or increase the limit at: ${consoleUrl}`, consoleUrl };
702
+ if (r.includes("Daily limit"))
703
+ return { explanation: "Daily spending limit would be exceeded.", action: `Wait until tomorrow, or increase the daily limit at: ${consoleUrl}`, consoleUrl };
704
+ if (r.includes("Approve exceeds limit"))
705
+ return { explanation: "The approve amount exceeds the configured approve limit.", action: `Reduce the amount, or increase the approve limit at: ${consoleUrl}`, consoleUrl };
706
+ if (r.includes("Cooldown"))
707
+ return { explanation: "Cooldown period has not elapsed since the last transaction.", action: "Wait for the cooldown to expire before retrying.", consoleUrl };
708
+ return { explanation: "Transaction was rejected by an on-chain security policy.", action: `Review your security settings at: ${consoleUrl}`, consoleUrl };
709
+ }
686
710
  var server = new import_mcp.McpServer({
687
711
  name: "shll-defi",
688
- version: "5.2.0"
712
+ version: "5.3.0"
689
713
  });
690
714
  server.tool(
691
715
  "portfolio",
@@ -850,7 +874,44 @@ server.tool(
850
874
  }
851
875
  for (const action of actions) {
852
876
  const sim = await policyClient.validate(tokenId, action);
853
- if (!sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason }) }] };
877
+ if (!sim.ok) {
878
+ if (useV3 && sim.reason?.includes("Approve spender not allowed") && v2Available) {
879
+ const v2Actions = [];
880
+ const v2Router = PANCAKE_V2_ROUTER;
881
+ const v2MinOut = v2Quote * BigInt(100 - slippage) / 100n;
882
+ if (!isNativeIn) {
883
+ const allow = await publicClient.readContract({ address: fromToken.address, abi: ERC20_ABI, functionName: "allowance", args: [vault, v2Router] }).catch(() => 0n);
884
+ if (allow < amountIn) {
885
+ v2Actions.push({ target: fromToken.address, value: 0n, data: (0, import_viem2.encodeFunctionData)({ abi: ERC20_ABI, functionName: "approve", args: [v2Router, amountIn] }) });
886
+ }
887
+ }
888
+ const v2Path = tokenInAddr.toLowerCase() !== WBNB.toLowerCase() && tokenOutAddr.toLowerCase() !== WBNB.toLowerCase() ? [tokenInAddr, WBNB, tokenOutAddr] : [tokenInAddr, tokenOutAddr];
889
+ if (isNativeIn) {
890
+ v2Actions.push({ target: v2Router, value: amountIn, data: (0, import_viem2.encodeFunctionData)({ abi: SWAP_EXACT_ETH_ABI, functionName: "swapExactETHForTokens", args: [v2MinOut, v2Path, vault, deadline] }) });
891
+ } else {
892
+ v2Actions.push({ target: v2Router, value: 0n, data: (0, import_viem2.encodeFunctionData)({ abi: SWAP_EXACT_TOKENS_ABI, functionName: "swapExactTokensForTokens", args: [amountIn, v2MinOut, v2Path, vault, deadline] }) });
893
+ }
894
+ for (const v2a of v2Actions) {
895
+ const v2Sim = await policyClient.validate(tokenId, v2a);
896
+ if (!v2Sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: v2Sim.reason, ...policyRejectionHelp(v2Sim.reason, token_id) }) }] };
897
+ }
898
+ const v2Result = v2Actions.length === 1 ? await policyClient.execute(tokenId, v2Actions[0], true) : await policyClient.executeBatch(tokenId, v2Actions, true);
899
+ return {
900
+ content: [{
901
+ type: "text",
902
+ text: JSON.stringify({
903
+ status: "success",
904
+ hash: v2Result.hash,
905
+ dex: "v2",
906
+ quote: v2Quote.toString(),
907
+ minOut: v2MinOut.toString(),
908
+ note: "V3 was rejected by policy (Approve spender not allowed). Auto-switched to V2."
909
+ })
910
+ }]
911
+ };
912
+ }
913
+ return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason, ...policyRejectionHelp(sim.reason, token_id) }) }] };
914
+ }
854
915
  }
855
916
  const result = actions.length === 1 ? await policyClient.execute(tokenId, actions[0], true) : await policyClient.executeBatch(tokenId, actions, true);
856
917
  return {
@@ -899,7 +960,7 @@ server.tool(
899
960
  }
900
961
  for (const action of actions) {
901
962
  const sim = await policyClient.validate(tokenId, action);
902
- if (!sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason }) }] };
963
+ if (!sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason, ...policyRejectionHelp(sim.reason, token_id) }) }] };
903
964
  }
904
965
  const result = actions.length === 1 ? await policyClient.execute(tokenId, actions[0], true) : await policyClient.executeBatch(tokenId, actions, true);
905
966
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", hash: result.hash, protocol: "venus", action: "supply", token: symbol, amount }) }] };
@@ -926,7 +987,7 @@ server.tool(
926
987
  const data = (0, import_viem2.encodeFunctionData)({ abi: VTOKEN_ABI, functionName: "redeemUnderlying", args: [amt] });
927
988
  const action = { target: vTokenAddr, value: 0n, data };
928
989
  const sim = await policyClient.validate(tokenId, action);
929
- if (!sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason }) }] };
990
+ if (!sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason, ...policyRejectionHelp(sim.reason, token_id) }) }] };
930
991
  const result = await policyClient.execute(tokenId, action, true);
931
992
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", hash: result.hash, protocol: "venus", action: "redeem", token: symbol, amount }) }] };
932
993
  }
@@ -991,7 +1052,7 @@ server.tool(
991
1052
  action = { target: tokenInfo.address, value: 0n, data };
992
1053
  }
993
1054
  const sim = await policyClient.validate(tokenId, action);
994
- if (!sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason }) }] };
1055
+ if (!sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason, ...policyRejectionHelp(sim.reason, token_id) }) }] };
995
1056
  const result = await policyClient.execute(tokenId, action, true);
996
1057
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", hash: result.hash, token, amount, to: recipient }) }] };
997
1058
  }
@@ -1078,7 +1139,7 @@ server.tool(
1078
1139
  const data = (0, import_viem2.encodeFunctionData)({ abi: WBNB_ABI, functionName: "deposit" });
1079
1140
  const action = { target: WBNB, value: amt, data };
1080
1141
  const sim = await policyClient.validate(tokenId, action);
1081
- if (!sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason }) }] };
1142
+ if (!sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason, ...policyRejectionHelp(sim.reason, token_id) }) }] };
1082
1143
  const result = await policyClient.execute(tokenId, action, true);
1083
1144
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", hash: result.hash, message: `Wrapped ${amount} BNB \u2192 WBNB` }) }] };
1084
1145
  }
@@ -1099,7 +1160,7 @@ server.tool(
1099
1160
  const data = (0, import_viem2.encodeFunctionData)({ abi: WBNB_ABI, functionName: "withdraw", args: [amt] });
1100
1161
  const action = { target: WBNB, value: 0n, data };
1101
1162
  const sim = await policyClient.validate(tokenId, action);
1102
- if (!sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason }) }] };
1163
+ if (!sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason, ...policyRejectionHelp(sim.reason, token_id) }) }] };
1103
1164
  const result = await policyClient.execute(tokenId, action, true);
1104
1165
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", hash: result.hash, message: `Unwrapped ${amount} WBNB \u2192 BNB` }) }] };
1105
1166
  }
@@ -1155,7 +1216,15 @@ server.tool(
1155
1216
  const [maxPerTx, maxPerDay, maxSlippageBps] = limits;
1156
1217
  const txBnb = (Number(maxPerTx) / 1e18).toFixed(4);
1157
1218
  const dayBnb = (Number(maxPerDay) / 1e18).toFixed(4);
1158
- entry.currentConfig = { maxPerTx: maxPerTx.toString(), maxPerTxBnb: txBnb, maxPerDay: maxPerDay.toString(), maxPerDayBnb: dayBnb, maxSlippageBps: maxSlippageBps.toString() };
1219
+ let tokenRestriction = {};
1220
+ try {
1221
+ const enabled = await publicClient.readContract({ address: p.address, abi: SPENDING_LIMIT_ABI, functionName: "tokenRestrictionEnabled", args: [tokenId] });
1222
+ const tokenList = await publicClient.readContract({ address: p.address, abi: SPENDING_LIMIT_ABI, functionName: "getTokenList", args: [tokenId] });
1223
+ tokenRestriction = { tokenRestrictionEnabled: enabled, whitelistedTokens: tokenList, whitelistedTokenCount: tokenList.length };
1224
+ summaryParts.push(enabled ? `Token whitelist ON (${tokenList.length} tokens)` : "Token whitelist OFF (any token allowed)");
1225
+ } catch {
1226
+ }
1227
+ entry.currentConfig = { maxPerTx: maxPerTx.toString(), maxPerTxBnb: txBnb, maxPerDay: maxPerDay.toString(), maxPerDayBnb: dayBnb, maxSlippageBps: maxSlippageBps.toString(), ...tokenRestriction };
1159
1228
  summaryParts.push(`Max ${txBnb} BNB/tx, ${dayBnb} BNB/day, slippage ${maxSlippageBps}bps`);
1160
1229
  } catch {
1161
1230
  }
@@ -1179,6 +1248,38 @@ server.tool(
1179
1248
  return { content: [{ type: "text", text: JSON.stringify({ tokenId: token_id, humanSummary, securityNote: "Operator wallet CANNOT withdraw vault funds or transfer Agent NFT.", policies: enriched }) }] };
1180
1249
  }
1181
1250
  );
1251
+ server.tool(
1252
+ "token_restriction",
1253
+ "Check token whitelist restriction status. Shows whether token trading is restricted and which tokens are whitelisted.",
1254
+ { token_id: import_zod.z.string().describe("Agent NFA Token ID") },
1255
+ async ({ token_id }) => {
1256
+ const { publicClient, policyClient } = createClients();
1257
+ const tokenId = BigInt(token_id);
1258
+ const policies = await policyClient.getPolicies(tokenId);
1259
+ const spendingPolicy = policies.find((p) => p.policyTypeName === "spending_limit");
1260
+ if (!spendingPolicy) {
1261
+ return { content: [{ type: "text", text: JSON.stringify({ tokenId: token_id, error: "No spending_limit policy found \u2014 token restriction is not available for this agent." }) }] };
1262
+ }
1263
+ try {
1264
+ const [enabled, tokenList] = await Promise.all([
1265
+ publicClient.readContract({ address: spendingPolicy.address, abi: SPENDING_LIMIT_ABI, functionName: "tokenRestrictionEnabled", args: [tokenId] }),
1266
+ publicClient.readContract({ address: spendingPolicy.address, abi: SPENDING_LIMIT_ABI, functionName: "getTokenList", args: [tokenId] })
1267
+ ]);
1268
+ const result = {
1269
+ tokenId: token_id,
1270
+ tokenRestrictionEnabled: enabled,
1271
+ status: enabled ? "ON \u2014 only whitelisted tokens can be traded" : "OFF \u2014 any token can be traded",
1272
+ whitelistedTokens: tokenList,
1273
+ whitelistedTokenCount: tokenList.length,
1274
+ manageUrl: `https://shll.run/agent/0xE98DCdbf370D7b52c9A2b88F79bEF514A5375a2b/${token_id}/console/safety`,
1275
+ note: enabled ? "To add/remove tokens or disable restriction, visit the management URL above (requires connected wallet as renter/owner)." : "Token restriction is disabled. The agent can trade any token. To enable, visit the management URL."
1276
+ };
1277
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
1278
+ } catch {
1279
+ return { content: [{ type: "text", text: JSON.stringify({ tokenId: token_id, error: "Failed to read token restriction \u2014 the SpendingLimitPolicy may not support this feature." }) }] };
1280
+ }
1281
+ }
1282
+ );
1182
1283
  server.tool(
1183
1284
  "status",
1184
1285
  "One-shot security overview: vault balance, operator status, policies, and recent activity",
package/dist/mcp.mjs CHANGED
@@ -124,7 +124,12 @@ var WBNB_ABI = [
124
124
  ];
125
125
  var SPENDING_LIMIT_ABI = [
126
126
  { type: "function", name: "setLimits", inputs: [{ name: "instanceId", type: "uint256" }, { name: "maxPerTx", type: "uint256" }, { name: "maxPerDay", type: "uint256" }, { name: "maxSlippageBps", type: "uint256" }], outputs: [], stateMutability: "nonpayable" },
127
- { type: "function", name: "instanceLimits", inputs: [{ name: "instanceId", type: "uint256" }], outputs: [{ name: "maxPerTx", type: "uint256" }, { name: "maxPerDay", type: "uint256" }, { name: "maxSlippageBps", type: "uint256" }], stateMutability: "view" }
127
+ { type: "function", name: "instanceLimits", inputs: [{ name: "instanceId", type: "uint256" }], outputs: [{ name: "maxPerTx", type: "uint256" }, { name: "maxPerDay", type: "uint256" }, { name: "maxSlippageBps", type: "uint256" }], stateMutability: "view" },
128
+ { type: "function", name: "tokenRestrictionEnabled", inputs: [{ name: "instanceId", type: "uint256" }], outputs: [{ name: "", type: "bool" }], stateMutability: "view" },
129
+ { type: "function", name: "getTokenList", inputs: [{ name: "instanceId", type: "uint256" }], outputs: [{ name: "", type: "address[]" }], stateMutability: "view" },
130
+ { type: "function", name: "addToken", inputs: [{ name: "instanceId", type: "uint256" }, { name: "token", type: "address" }], outputs: [], stateMutability: "nonpayable" },
131
+ { type: "function", name: "removeToken", inputs: [{ name: "instanceId", type: "uint256" }, { name: "token", type: "address" }], outputs: [], stateMutability: "nonpayable" },
132
+ { type: "function", name: "setTokenRestriction", inputs: [{ name: "instanceId", type: "uint256" }, { name: "enabled", type: "bool" }], outputs: [], stateMutability: "nonpayable" }
128
133
  ];
129
134
  var COOLDOWN_ABI = [
130
135
  { type: "function", name: "setCooldown", inputs: [{ name: "instanceId", type: "uint256" }, { name: "seconds_", type: "uint256" }], outputs: [], stateMutability: "nonpayable" },
@@ -194,9 +199,28 @@ async function checkAgentExpiry(tokenId) {
194
199
  }
195
200
  return { expired: false };
196
201
  }
202
+ function policyRejectionHelp(reason, tokenId) {
203
+ const consoleUrl = `https://shll.run/agent/0xE98DCdbf370D7b52c9A2b88F79bEF514A5375a2b/${tokenId}/console/safety`;
204
+ const r = reason ?? "";
205
+ if (r.includes("Approve spender not allowed"))
206
+ return { explanation: "The DEX router address is not in the approved spender whitelist. This is a platform-level security setting.", action: "Contact the Agent owner to approve this router, or use a different DEX.", consoleUrl };
207
+ if (r.includes("Target not allowed"))
208
+ return { explanation: "The contract address is not in the DeFi target whitelist.", action: "Enable the corresponding DeFi Pack in Console > Safety, or contact the Agent owner.", consoleUrl };
209
+ if (r.includes("Token not in whitelist"))
210
+ return { explanation: "Token restriction is ON and this token is not whitelisted.", action: `Add the token to the whitelist or disable token restriction at: ${consoleUrl}`, consoleUrl };
211
+ if (r.includes("Exceeds per-tx limit"))
212
+ return { explanation: "Transaction value exceeds the per-transaction spending limit.", action: `Reduce the amount, or increase the limit at: ${consoleUrl}`, consoleUrl };
213
+ if (r.includes("Daily limit"))
214
+ return { explanation: "Daily spending limit would be exceeded.", action: `Wait until tomorrow, or increase the daily limit at: ${consoleUrl}`, consoleUrl };
215
+ if (r.includes("Approve exceeds limit"))
216
+ return { explanation: "The approve amount exceeds the configured approve limit.", action: `Reduce the amount, or increase the approve limit at: ${consoleUrl}`, consoleUrl };
217
+ if (r.includes("Cooldown"))
218
+ return { explanation: "Cooldown period has not elapsed since the last transaction.", action: "Wait for the cooldown to expire before retrying.", consoleUrl };
219
+ return { explanation: "Transaction was rejected by an on-chain security policy.", action: `Review your security settings at: ${consoleUrl}`, consoleUrl };
220
+ }
197
221
  var server = new McpServer({
198
222
  name: "shll-defi",
199
- version: "5.2.0"
223
+ version: "5.3.0"
200
224
  });
201
225
  server.tool(
202
226
  "portfolio",
@@ -361,7 +385,44 @@ server.tool(
361
385
  }
362
386
  for (const action of actions) {
363
387
  const sim = await policyClient.validate(tokenId, action);
364
- if (!sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason }) }] };
388
+ if (!sim.ok) {
389
+ if (useV3 && sim.reason?.includes("Approve spender not allowed") && v2Available) {
390
+ const v2Actions = [];
391
+ const v2Router = PANCAKE_V2_ROUTER;
392
+ const v2MinOut = v2Quote * BigInt(100 - slippage) / 100n;
393
+ if (!isNativeIn) {
394
+ const allow = await publicClient.readContract({ address: fromToken.address, abi: ERC20_ABI, functionName: "allowance", args: [vault, v2Router] }).catch(() => 0n);
395
+ if (allow < amountIn) {
396
+ v2Actions.push({ target: fromToken.address, value: 0n, data: encodeFunctionData({ abi: ERC20_ABI, functionName: "approve", args: [v2Router, amountIn] }) });
397
+ }
398
+ }
399
+ const v2Path = tokenInAddr.toLowerCase() !== WBNB.toLowerCase() && tokenOutAddr.toLowerCase() !== WBNB.toLowerCase() ? [tokenInAddr, WBNB, tokenOutAddr] : [tokenInAddr, tokenOutAddr];
400
+ if (isNativeIn) {
401
+ v2Actions.push({ target: v2Router, value: amountIn, data: encodeFunctionData({ abi: SWAP_EXACT_ETH_ABI, functionName: "swapExactETHForTokens", args: [v2MinOut, v2Path, vault, deadline] }) });
402
+ } else {
403
+ v2Actions.push({ target: v2Router, value: 0n, data: encodeFunctionData({ abi: SWAP_EXACT_TOKENS_ABI, functionName: "swapExactTokensForTokens", args: [amountIn, v2MinOut, v2Path, vault, deadline] }) });
404
+ }
405
+ for (const v2a of v2Actions) {
406
+ const v2Sim = await policyClient.validate(tokenId, v2a);
407
+ if (!v2Sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: v2Sim.reason, ...policyRejectionHelp(v2Sim.reason, token_id) }) }] };
408
+ }
409
+ const v2Result = v2Actions.length === 1 ? await policyClient.execute(tokenId, v2Actions[0], true) : await policyClient.executeBatch(tokenId, v2Actions, true);
410
+ return {
411
+ content: [{
412
+ type: "text",
413
+ text: JSON.stringify({
414
+ status: "success",
415
+ hash: v2Result.hash,
416
+ dex: "v2",
417
+ quote: v2Quote.toString(),
418
+ minOut: v2MinOut.toString(),
419
+ note: "V3 was rejected by policy (Approve spender not allowed). Auto-switched to V2."
420
+ })
421
+ }]
422
+ };
423
+ }
424
+ return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason, ...policyRejectionHelp(sim.reason, token_id) }) }] };
425
+ }
365
426
  }
366
427
  const result = actions.length === 1 ? await policyClient.execute(tokenId, actions[0], true) : await policyClient.executeBatch(tokenId, actions, true);
367
428
  return {
@@ -410,7 +471,7 @@ server.tool(
410
471
  }
411
472
  for (const action of actions) {
412
473
  const sim = await policyClient.validate(tokenId, action);
413
- if (!sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason }) }] };
474
+ if (!sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason, ...policyRejectionHelp(sim.reason, token_id) }) }] };
414
475
  }
415
476
  const result = actions.length === 1 ? await policyClient.execute(tokenId, actions[0], true) : await policyClient.executeBatch(tokenId, actions, true);
416
477
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", hash: result.hash, protocol: "venus", action: "supply", token: symbol, amount }) }] };
@@ -437,7 +498,7 @@ server.tool(
437
498
  const data = encodeFunctionData({ abi: VTOKEN_ABI, functionName: "redeemUnderlying", args: [amt] });
438
499
  const action = { target: vTokenAddr, value: 0n, data };
439
500
  const sim = await policyClient.validate(tokenId, action);
440
- if (!sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason }) }] };
501
+ if (!sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason, ...policyRejectionHelp(sim.reason, token_id) }) }] };
441
502
  const result = await policyClient.execute(tokenId, action, true);
442
503
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", hash: result.hash, protocol: "venus", action: "redeem", token: symbol, amount }) }] };
443
504
  }
@@ -502,7 +563,7 @@ server.tool(
502
563
  action = { target: tokenInfo.address, value: 0n, data };
503
564
  }
504
565
  const sim = await policyClient.validate(tokenId, action);
505
- if (!sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason }) }] };
566
+ if (!sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason, ...policyRejectionHelp(sim.reason, token_id) }) }] };
506
567
  const result = await policyClient.execute(tokenId, action, true);
507
568
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", hash: result.hash, token, amount, to: recipient }) }] };
508
569
  }
@@ -589,7 +650,7 @@ server.tool(
589
650
  const data = encodeFunctionData({ abi: WBNB_ABI, functionName: "deposit" });
590
651
  const action = { target: WBNB, value: amt, data };
591
652
  const sim = await policyClient.validate(tokenId, action);
592
- if (!sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason }) }] };
653
+ if (!sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason, ...policyRejectionHelp(sim.reason, token_id) }) }] };
593
654
  const result = await policyClient.execute(tokenId, action, true);
594
655
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", hash: result.hash, message: `Wrapped ${amount} BNB \u2192 WBNB` }) }] };
595
656
  }
@@ -610,7 +671,7 @@ server.tool(
610
671
  const data = encodeFunctionData({ abi: WBNB_ABI, functionName: "withdraw", args: [amt] });
611
672
  const action = { target: WBNB, value: 0n, data };
612
673
  const sim = await policyClient.validate(tokenId, action);
613
- if (!sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason }) }] };
674
+ if (!sim.ok) return { content: [{ type: "text", text: JSON.stringify({ status: "rejected", reason: sim.reason, ...policyRejectionHelp(sim.reason, token_id) }) }] };
614
675
  const result = await policyClient.execute(tokenId, action, true);
615
676
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", hash: result.hash, message: `Unwrapped ${amount} WBNB \u2192 BNB` }) }] };
616
677
  }
@@ -666,7 +727,15 @@ server.tool(
666
727
  const [maxPerTx, maxPerDay, maxSlippageBps] = limits;
667
728
  const txBnb = (Number(maxPerTx) / 1e18).toFixed(4);
668
729
  const dayBnb = (Number(maxPerDay) / 1e18).toFixed(4);
669
- entry.currentConfig = { maxPerTx: maxPerTx.toString(), maxPerTxBnb: txBnb, maxPerDay: maxPerDay.toString(), maxPerDayBnb: dayBnb, maxSlippageBps: maxSlippageBps.toString() };
730
+ let tokenRestriction = {};
731
+ try {
732
+ const enabled = await publicClient.readContract({ address: p.address, abi: SPENDING_LIMIT_ABI, functionName: "tokenRestrictionEnabled", args: [tokenId] });
733
+ const tokenList = await publicClient.readContract({ address: p.address, abi: SPENDING_LIMIT_ABI, functionName: "getTokenList", args: [tokenId] });
734
+ tokenRestriction = { tokenRestrictionEnabled: enabled, whitelistedTokens: tokenList, whitelistedTokenCount: tokenList.length };
735
+ summaryParts.push(enabled ? `Token whitelist ON (${tokenList.length} tokens)` : "Token whitelist OFF (any token allowed)");
736
+ } catch {
737
+ }
738
+ entry.currentConfig = { maxPerTx: maxPerTx.toString(), maxPerTxBnb: txBnb, maxPerDay: maxPerDay.toString(), maxPerDayBnb: dayBnb, maxSlippageBps: maxSlippageBps.toString(), ...tokenRestriction };
670
739
  summaryParts.push(`Max ${txBnb} BNB/tx, ${dayBnb} BNB/day, slippage ${maxSlippageBps}bps`);
671
740
  } catch {
672
741
  }
@@ -690,6 +759,38 @@ server.tool(
690
759
  return { content: [{ type: "text", text: JSON.stringify({ tokenId: token_id, humanSummary, securityNote: "Operator wallet CANNOT withdraw vault funds or transfer Agent NFT.", policies: enriched }) }] };
691
760
  }
692
761
  );
762
+ server.tool(
763
+ "token_restriction",
764
+ "Check token whitelist restriction status. Shows whether token trading is restricted and which tokens are whitelisted.",
765
+ { token_id: z.string().describe("Agent NFA Token ID") },
766
+ async ({ token_id }) => {
767
+ const { publicClient, policyClient } = createClients();
768
+ const tokenId = BigInt(token_id);
769
+ const policies = await policyClient.getPolicies(tokenId);
770
+ const spendingPolicy = policies.find((p) => p.policyTypeName === "spending_limit");
771
+ if (!spendingPolicy) {
772
+ return { content: [{ type: "text", text: JSON.stringify({ tokenId: token_id, error: "No spending_limit policy found \u2014 token restriction is not available for this agent." }) }] };
773
+ }
774
+ try {
775
+ const [enabled, tokenList] = await Promise.all([
776
+ publicClient.readContract({ address: spendingPolicy.address, abi: SPENDING_LIMIT_ABI, functionName: "tokenRestrictionEnabled", args: [tokenId] }),
777
+ publicClient.readContract({ address: spendingPolicy.address, abi: SPENDING_LIMIT_ABI, functionName: "getTokenList", args: [tokenId] })
778
+ ]);
779
+ const result = {
780
+ tokenId: token_id,
781
+ tokenRestrictionEnabled: enabled,
782
+ status: enabled ? "ON \u2014 only whitelisted tokens can be traded" : "OFF \u2014 any token can be traded",
783
+ whitelistedTokens: tokenList,
784
+ whitelistedTokenCount: tokenList.length,
785
+ manageUrl: `https://shll.run/agent/0xE98DCdbf370D7b52c9A2b88F79bEF514A5375a2b/${token_id}/console/safety`,
786
+ note: enabled ? "To add/remove tokens or disable restriction, visit the management URL above (requires connected wallet as renter/owner)." : "Token restriction is disabled. The agent can trade any token. To enable, visit the management URL."
787
+ };
788
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
789
+ } catch {
790
+ return { content: [{ type: "text", text: JSON.stringify({ tokenId: token_id, error: "Failed to read token restriction \u2014 the SpendingLimitPolicy may not support this feature." }) }] };
791
+ }
792
+ }
793
+ );
693
794
  server.tool(
694
795
  "status",
695
796
  "One-shot security overview: vault balance, operator status, policies, and recent activity",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shll-skills",
3
- "version": "5.2.1",
3
+ "version": "5.3.1",
4
4
  "description": "SHLL DeFi Agent — CLI + MCP Server for BSC",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -24,4 +24,4 @@
24
24
  "tsup": "^8.0.2",
25
25
  "typescript": "^5.4.5"
26
26
  }
27
- }
27
+ }
package/src/mcp.ts CHANGED
@@ -153,6 +153,11 @@ const WBNB_ABI = [
153
153
  const SPENDING_LIMIT_ABI = [
154
154
  { type: "function" as const, name: "setLimits", inputs: [{ name: "instanceId", type: "uint256" }, { name: "maxPerTx", type: "uint256" }, { name: "maxPerDay", type: "uint256" }, { name: "maxSlippageBps", type: "uint256" }], outputs: [], stateMutability: "nonpayable" as const },
155
155
  { type: "function" as const, name: "instanceLimits", inputs: [{ name: "instanceId", type: "uint256" }], outputs: [{ name: "maxPerTx", type: "uint256" }, { name: "maxPerDay", type: "uint256" }, { name: "maxSlippageBps", type: "uint256" }], stateMutability: "view" as const },
156
+ { type: "function" as const, name: "tokenRestrictionEnabled", inputs: [{ name: "instanceId", type: "uint256" }], outputs: [{ name: "", type: "bool" }], stateMutability: "view" as const },
157
+ { type: "function" as const, name: "getTokenList", inputs: [{ name: "instanceId", type: "uint256" }], outputs: [{ name: "", type: "address[]" }], stateMutability: "view" as const },
158
+ { type: "function" as const, name: "addToken", inputs: [{ name: "instanceId", type: "uint256" }, { name: "token", type: "address" }], outputs: [], stateMutability: "nonpayable" as const },
159
+ { type: "function" as const, name: "removeToken", inputs: [{ name: "instanceId", type: "uint256" }, { name: "token", type: "address" }], outputs: [], stateMutability: "nonpayable" as const },
160
+ { type: "function" as const, name: "setTokenRestriction", inputs: [{ name: "instanceId", type: "uint256" }, { name: "enabled", type: "bool" }], outputs: [], stateMutability: "nonpayable" as const },
156
161
  ] as const;
157
162
 
158
163
  const COOLDOWN_ABI = [
@@ -234,13 +239,34 @@ async function checkAgentExpiry(tokenId: bigint) {
234
239
  return { expired: false };
235
240
  }
236
241
 
242
+ // Policy rejection → actionable user guidance
243
+ function policyRejectionHelp(reason: string | undefined, tokenId: string): Record<string, string> {
244
+ const consoleUrl = `https://shll.run/agent/0xE98DCdbf370D7b52c9A2b88F79bEF514A5375a2b/${tokenId}/console/safety`;
245
+ const r = reason ?? "";
246
+ if (r.includes("Approve spender not allowed"))
247
+ return { explanation: "The DEX router address is not in the approved spender whitelist. This is a platform-level security setting.", action: "Contact the Agent owner to approve this router, or use a different DEX.", consoleUrl };
248
+ if (r.includes("Target not allowed"))
249
+ return { explanation: "The contract address is not in the DeFi target whitelist.", action: "Enable the corresponding DeFi Pack in Console > Safety, or contact the Agent owner.", consoleUrl };
250
+ if (r.includes("Token not in whitelist"))
251
+ return { explanation: "Token restriction is ON and this token is not whitelisted.", action: `Add the token to the whitelist or disable token restriction at: ${consoleUrl}`, consoleUrl };
252
+ if (r.includes("Exceeds per-tx limit"))
253
+ return { explanation: "Transaction value exceeds the per-transaction spending limit.", action: `Reduce the amount, or increase the limit at: ${consoleUrl}`, consoleUrl };
254
+ if (r.includes("Daily limit"))
255
+ return { explanation: "Daily spending limit would be exceeded.", action: `Wait until tomorrow, or increase the daily limit at: ${consoleUrl}`, consoleUrl };
256
+ if (r.includes("Approve exceeds limit"))
257
+ return { explanation: "The approve amount exceeds the configured approve limit.", action: `Reduce the amount, or increase the approve limit at: ${consoleUrl}`, consoleUrl };
258
+ if (r.includes("Cooldown"))
259
+ return { explanation: "Cooldown period has not elapsed since the last transaction.", action: "Wait for the cooldown to expire before retrying.", consoleUrl };
260
+ return { explanation: "Transaction was rejected by an on-chain security policy.", action: `Review your security settings at: ${consoleUrl}`, consoleUrl };
261
+ }
262
+
237
263
  // ═══════════════════════════════════════════════════════
238
264
  // MCP Server
239
265
  // ═══════════════════════════════════════════════════════
240
266
 
241
267
  const server = new McpServer({
242
268
  name: "shll-defi",
243
- version: "5.2.0",
269
+ version: "5.3.0",
244
270
  });
245
271
 
246
272
  // ── Tool: portfolio ─────────────────────────────────────
@@ -421,10 +447,48 @@ server.tool(
421
447
  }
422
448
  }
423
449
 
424
- // Validate + execute
450
+ // Validate + execute (with V3→V2 fallback on approve rejection)
425
451
  for (const action of actions) {
426
452
  const sim = await policyClient.validate(tokenId, action);
427
- if (!sim.ok) return { content: [{ type: "text" as const, text: JSON.stringify({ status: "rejected", reason: sim.reason }) }] };
453
+ if (!sim.ok) {
454
+ // If V3 approve was rejected, try V2 fallback
455
+ if (useV3 && sim.reason?.includes("Approve spender not allowed") && v2Available) {
456
+ const v2Actions: Action[] = [];
457
+ const v2Router = PANCAKE_V2_ROUTER as Address;
458
+ const v2MinOut = (v2Quote * BigInt(100 - slippage)) / 100n;
459
+ if (!isNativeIn) {
460
+ const allow = await publicClient.readContract({ address: fromToken.address, abi: ERC20_ABI, functionName: "allowance", args: [vault, v2Router] }).catch(() => 0n);
461
+ if (allow < amountIn) {
462
+ v2Actions.push({ target: fromToken.address, value: 0n, data: encodeFunctionData({ abi: ERC20_ABI, functionName: "approve", args: [v2Router, amountIn] }) });
463
+ }
464
+ }
465
+ const v2Path: Address[] = tokenInAddr.toLowerCase() !== WBNB.toLowerCase() && tokenOutAddr.toLowerCase() !== WBNB.toLowerCase()
466
+ ? [tokenInAddr, WBNB as Address, tokenOutAddr] : [tokenInAddr, tokenOutAddr];
467
+ if (isNativeIn) {
468
+ v2Actions.push({ target: v2Router, value: amountIn, data: encodeFunctionData({ abi: SWAP_EXACT_ETH_ABI, functionName: "swapExactETHForTokens", args: [v2MinOut, v2Path, vault, deadline] }) });
469
+ } else {
470
+ v2Actions.push({ target: v2Router, value: 0n, data: encodeFunctionData({ abi: SWAP_EXACT_TOKENS_ABI, functionName: "swapExactTokensForTokens", args: [amountIn, v2MinOut, v2Path, vault, deadline] }) });
471
+ }
472
+ // Validate V2 fallback
473
+ for (const v2a of v2Actions) {
474
+ const v2Sim = await policyClient.validate(tokenId, v2a);
475
+ if (!v2Sim.ok) return { content: [{ type: "text" as const, text: JSON.stringify({ status: "rejected", reason: v2Sim.reason, ...policyRejectionHelp(v2Sim.reason, token_id) }) }] };
476
+ }
477
+ const v2Result = v2Actions.length === 1
478
+ ? await policyClient.execute(tokenId, v2Actions[0], true)
479
+ : await policyClient.executeBatch(tokenId, v2Actions, true);
480
+ return {
481
+ content: [{
482
+ type: "text" as const, text: JSON.stringify({
483
+ status: "success", hash: v2Result.hash, dex: "v2",
484
+ quote: v2Quote.toString(), minOut: v2MinOut.toString(),
485
+ note: "V3 was rejected by policy (Approve spender not allowed). Auto-switched to V2.",
486
+ })
487
+ }]
488
+ };
489
+ }
490
+ return { content: [{ type: "text" as const, text: JSON.stringify({ status: "rejected", reason: sim.reason, ...policyRejectionHelp(sim.reason, token_id) }) }] };
491
+ }
428
492
  }
429
493
 
430
494
  const result = actions.length === 1
@@ -477,7 +541,7 @@ server.tool(
477
541
 
478
542
  for (const action of actions) {
479
543
  const sim = await policyClient.validate(tokenId, action);
480
- if (!sim.ok) return { content: [{ type: "text" as const, text: JSON.stringify({ status: "rejected", reason: sim.reason }) }] };
544
+ if (!sim.ok) return { content: [{ type: "text" as const, text: JSON.stringify({ status: "rejected", reason: sim.reason, ...policyRejectionHelp(sim.reason, token_id) }) }] };
481
545
  }
482
546
 
483
547
  const result = actions.length === 1
@@ -511,7 +575,7 @@ server.tool(
511
575
  const action: Action = { target: vTokenAddr, value: 0n, data };
512
576
 
513
577
  const sim = await policyClient.validate(tokenId, action);
514
- if (!sim.ok) return { content: [{ type: "text" as const, text: JSON.stringify({ status: "rejected", reason: sim.reason }) }] };
578
+ if (!sim.ok) return { content: [{ type: "text" as const, text: JSON.stringify({ status: "rejected", reason: sim.reason, ...policyRejectionHelp(sim.reason, token_id) }) }] };
515
579
 
516
580
  const result = await policyClient.execute(tokenId, action, true);
517
581
  return { content: [{ type: "text" as const, text: JSON.stringify({ status: "success", hash: result.hash, protocol: "venus", action: "redeem", token: symbol, amount }) }] };
@@ -585,7 +649,7 @@ server.tool(
585
649
  }
586
650
 
587
651
  const sim = await policyClient.validate(tokenId, action);
588
- if (!sim.ok) return { content: [{ type: "text" as const, text: JSON.stringify({ status: "rejected", reason: sim.reason }) }] };
652
+ if (!sim.ok) return { content: [{ type: "text" as const, text: JSON.stringify({ status: "rejected", reason: sim.reason, ...policyRejectionHelp(sim.reason, token_id) }) }] };
589
653
 
590
654
  const result = await policyClient.execute(tokenId, action, true);
591
655
  return { content: [{ type: "text" as const, text: JSON.stringify({ status: "success", hash: result.hash, token, amount, to: recipient }) }] };
@@ -678,7 +742,7 @@ server.tool(
678
742
  const action: Action = { target: WBNB as Address, value: amt, data };
679
743
 
680
744
  const sim = await policyClient.validate(tokenId, action);
681
- if (!sim.ok) return { content: [{ type: "text" as const, text: JSON.stringify({ status: "rejected", reason: sim.reason }) }] };
745
+ if (!sim.ok) return { content: [{ type: "text" as const, text: JSON.stringify({ status: "rejected", reason: sim.reason, ...policyRejectionHelp(sim.reason, token_id) }) }] };
682
746
 
683
747
  const result = await policyClient.execute(tokenId, action, true);
684
748
  return { content: [{ type: "text" as const, text: JSON.stringify({ status: "success", hash: result.hash, message: `Wrapped ${amount} BNB → WBNB` }) }] };
@@ -702,7 +766,7 @@ server.tool(
702
766
  const action: Action = { target: WBNB as Address, value: 0n, data };
703
767
 
704
768
  const sim = await policyClient.validate(tokenId, action);
705
- if (!sim.ok) return { content: [{ type: "text" as const, text: JSON.stringify({ status: "rejected", reason: sim.reason }) }] };
769
+ if (!sim.ok) return { content: [{ type: "text" as const, text: JSON.stringify({ status: "rejected", reason: sim.reason, ...policyRejectionHelp(sim.reason, token_id) }) }] };
706
770
 
707
771
  const result = await policyClient.execute(tokenId, action, true);
708
772
  return { content: [{ type: "text" as const, text: JSON.stringify({ status: "success", hash: result.hash, message: `Unwrapped ${amount} WBNB → BNB` }) }] };
@@ -768,7 +832,15 @@ server.tool(
768
832
  const [maxPerTx, maxPerDay, maxSlippageBps] = limits;
769
833
  const txBnb = (Number(maxPerTx) / 1e18).toFixed(4);
770
834
  const dayBnb = (Number(maxPerDay) / 1e18).toFixed(4);
771
- entry.currentConfig = { maxPerTx: maxPerTx.toString(), maxPerTxBnb: txBnb, maxPerDay: maxPerDay.toString(), maxPerDayBnb: dayBnb, maxSlippageBps: maxSlippageBps.toString() };
835
+ // Also read token restriction status
836
+ let tokenRestriction: Record<string, unknown> = {};
837
+ try {
838
+ const enabled = await publicClient.readContract({ address: p.address, abi: SPENDING_LIMIT_ABI, functionName: "tokenRestrictionEnabled", args: [tokenId] }) as boolean;
839
+ const tokenList = await publicClient.readContract({ address: p.address, abi: SPENDING_LIMIT_ABI, functionName: "getTokenList", args: [tokenId] }) as string[];
840
+ tokenRestriction = { tokenRestrictionEnabled: enabled, whitelistedTokens: tokenList, whitelistedTokenCount: tokenList.length };
841
+ summaryParts.push(enabled ? `Token whitelist ON (${tokenList.length} tokens)` : "Token whitelist OFF (any token allowed)");
842
+ } catch { /* token restriction not available on this version */ }
843
+ entry.currentConfig = { maxPerTx: maxPerTx.toString(), maxPerTxBnb: txBnb, maxPerDay: maxPerDay.toString(), maxPerDayBnb: dayBnb, maxSlippageBps: maxSlippageBps.toString(), ...tokenRestriction };
772
844
  summaryParts.push(`Max ${txBnb} BNB/tx, ${dayBnb} BNB/day, slippage ${maxSlippageBps}bps`);
773
845
  } catch { /* policy read failed */ }
774
846
  }
@@ -793,6 +865,42 @@ server.tool(
793
865
  }
794
866
  );
795
867
 
868
+ // ── Tool: token_restriction ─────────────────────────────
869
+ server.tool(
870
+ "token_restriction",
871
+ "Check token whitelist restriction status. Shows whether token trading is restricted and which tokens are whitelisted.",
872
+ { token_id: z.string().describe("Agent NFA Token ID") },
873
+ async ({ token_id }) => {
874
+ const { publicClient, policyClient } = createClients();
875
+ const tokenId = BigInt(token_id);
876
+ const policies = await policyClient.getPolicies(tokenId);
877
+ const spendingPolicy = policies.find(p => p.policyTypeName === "spending_limit");
878
+ if (!spendingPolicy) {
879
+ return { content: [{ type: "text" as const, text: JSON.stringify({ tokenId: token_id, error: "No spending_limit policy found — token restriction is not available for this agent." }) }] };
880
+ }
881
+ try {
882
+ const [enabled, tokenList] = await Promise.all([
883
+ publicClient.readContract({ address: spendingPolicy.address, abi: SPENDING_LIMIT_ABI, functionName: "tokenRestrictionEnabled", args: [tokenId] }) as Promise<boolean>,
884
+ publicClient.readContract({ address: spendingPolicy.address, abi: SPENDING_LIMIT_ABI, functionName: "getTokenList", args: [tokenId] }) as Promise<string[]>,
885
+ ]);
886
+ const result = {
887
+ tokenId: token_id,
888
+ tokenRestrictionEnabled: enabled,
889
+ status: enabled ? "ON — only whitelisted tokens can be traded" : "OFF — any token can be traded",
890
+ whitelistedTokens: tokenList,
891
+ whitelistedTokenCount: tokenList.length,
892
+ manageUrl: `https://shll.run/agent/0xE98DCdbf370D7b52c9A2b88F79bEF514A5375a2b/${token_id}/console/safety`,
893
+ note: enabled
894
+ ? "To add/remove tokens or disable restriction, visit the management URL above (requires connected wallet as renter/owner)."
895
+ : "Token restriction is disabled. The agent can trade any token. To enable, visit the management URL.",
896
+ };
897
+ return { content: [{ type: "text" as const, text: JSON.stringify(result) }] };
898
+ } catch {
899
+ return { content: [{ type: "text" as const, text: JSON.stringify({ tokenId: token_id, error: "Failed to read token restriction — the SpendingLimitPolicy may not support this feature." }) }] };
900
+ }
901
+ }
902
+ );
903
+
796
904
  // ── Tool: status ────────────────────────────────────────
797
905
  server.tool(
798
906
  "status",