shll-skills 5.1.1 → 5.2.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
@@ -641,9 +641,51 @@ function createClients() {
641
641
  });
642
642
  return { account, publicClient, policyClient, config };
643
643
  }
644
+ var AGENT_NFA_EXPIRY_ABI = [
645
+ { name: "operatorExpiresOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
646
+ { name: "userExpires", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] }
647
+ ];
648
+ async function checkAgentExpiry(tokenId) {
649
+ const config = getConfig();
650
+ const pc = (0, import_viem2.createPublicClient)({ chain: import_chains2.bsc, transport: (0, import_viem2.http)(config.rpc) });
651
+ const [operatorExpires, userExpires] = await Promise.all([
652
+ pc.readContract({ address: config.nfa, abi: AGENT_NFA_EXPIRY_ABI, functionName: "operatorExpiresOf", args: [tokenId] }),
653
+ pc.readContract({ address: config.nfa, abi: AGENT_NFA_EXPIRY_ABI, functionName: "userExpires", args: [tokenId] })
654
+ ]);
655
+ const now = BigInt(Math.floor(Date.now() / 1e3));
656
+ if (now > operatorExpires) {
657
+ return {
658
+ expired: true,
659
+ content: [{
660
+ type: "text",
661
+ text: JSON.stringify({
662
+ status: "error",
663
+ message: `Agent token-id ${tokenId} operator authorization has EXPIRED (expired at ${new Date(Number(operatorExpires) * 1e3).toISOString()}). Please renew at https://shll.run/me or use a different token-id.`,
664
+ expiredAt: new Date(Number(operatorExpires) * 1e3).toISOString(),
665
+ action: "renew"
666
+ })
667
+ }]
668
+ };
669
+ }
670
+ if (now > userExpires) {
671
+ return {
672
+ expired: true,
673
+ content: [{
674
+ type: "text",
675
+ text: JSON.stringify({
676
+ status: "error",
677
+ message: `Agent token-id ${tokenId} rental has EXPIRED (expired at ${new Date(Number(userExpires) * 1e3).toISOString()}). Please renew at https://shll.run/me or use a different token-id.`,
678
+ expiredAt: new Date(Number(userExpires) * 1e3).toISOString(),
679
+ action: "renew"
680
+ })
681
+ }]
682
+ };
683
+ }
684
+ return { expired: false };
685
+ }
644
686
  var server = new import_mcp.McpServer({
645
687
  name: "shll-defi",
646
- version: "5.0.0"
688
+ version: "5.2.0"
647
689
  });
648
690
  server.tool(
649
691
  "portfolio",
@@ -741,6 +783,8 @@ server.tool(
741
783
  async ({ token_id, from, to, amount, dex, slippage }) => {
742
784
  const { publicClient, policyClient } = createClients();
743
785
  const tokenId = BigInt(token_id);
786
+ const expiryCheck = await checkAgentExpiry(tokenId);
787
+ if (expiryCheck.expired) return { content: expiryCheck.content };
744
788
  const vault = await policyClient.getVault(tokenId);
745
789
  const fromToken = resolveToken(from);
746
790
  const toToken = resolveToken(to);
@@ -834,6 +878,8 @@ server.tool(
834
878
  async ({ token_id, token, amount }) => {
835
879
  const { publicClient, policyClient } = createClients();
836
880
  const tokenId = BigInt(token_id);
881
+ const expiryCheck = await checkAgentExpiry(tokenId);
882
+ if (expiryCheck.expired) return { content: expiryCheck.content };
837
883
  const symbol = token.toUpperCase();
838
884
  const vTokenAddr = VENUS_VTOKENS[symbol];
839
885
  if (!vTokenAddr) return { content: [{ type: "text", text: JSON.stringify({ error: `Unsupported: ${symbol}. Use: ${Object.keys(VENUS_VTOKENS).join(", ")}` }) }] };
@@ -870,6 +916,8 @@ server.tool(
870
916
  async ({ token_id, token, amount }) => {
871
917
  const { policyClient } = createClients();
872
918
  const tokenId = BigInt(token_id);
919
+ const expiryCheck = await checkAgentExpiry(tokenId);
920
+ if (expiryCheck.expired) return { content: expiryCheck.content };
873
921
  const symbol = token.toUpperCase();
874
922
  const vTokenAddr = VENUS_VTOKENS[symbol];
875
923
  if (!vTokenAddr) return { content: [{ type: "text", text: JSON.stringify({ error: `Unsupported: ${symbol}` }) }] };
@@ -926,6 +974,8 @@ server.tool(
926
974
  async ({ token_id, token, amount, to }) => {
927
975
  const { policyClient } = createClients();
928
976
  const tokenId = BigInt(token_id);
977
+ const expiryCheck = await checkAgentExpiry(tokenId);
978
+ if (expiryCheck.expired) return { content: expiryCheck.content };
929
979
  const tokenInfo = resolveToken(token);
930
980
  const amt = parseAmount(amount, tokenInfo.decimals);
931
981
  const recipient = to;
@@ -946,17 +996,14 @@ server.tool(
946
996
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", hash: result.hash, token, amount, to: recipient }) }] };
947
997
  }
948
998
  );
949
- var OPERATOR_OF_ABI = [{
950
- type: "function",
951
- name: "operatorOf",
952
- inputs: [{ name: "tokenId", type: "uint256" }],
953
- outputs: [{ name: "", type: "address" }],
954
- stateMutability: "view"
955
- }];
999
+ var MY_AGENTS_ABI = [
1000
+ { type: "function", name: "operatorOf", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }], stateMutability: "view" },
1001
+ { type: "function", name: "operatorExpiresOf", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }], stateMutability: "view" }
1002
+ ];
956
1003
  var DEFAULT_INDEXER = "https://indexer-mainnet.shll.run";
957
1004
  server.tool(
958
1005
  "my_agents",
959
- "List all agents where the current operator key is authorized. Returns token IDs, vault addresses, and agent types. Call this first if the user does not specify a token ID.",
1006
+ "List all agents where the current operator key is or was authorized. Returns active agents and expired agents that need renewal.",
960
1007
  {},
961
1008
  async () => {
962
1009
  const { account, publicClient, config } = createClients();
@@ -973,18 +1020,34 @@ server.tool(
973
1020
  agents.map(async (a) => {
974
1021
  const tokenId = BigInt(a.tokenId);
975
1022
  try {
976
- const op = await publicClient.readContract({
977
- address: nfaAddr,
978
- abi: OPERATOR_OF_ABI,
979
- functionName: "operatorOf",
980
- args: [tokenId]
981
- });
982
- return op.toLowerCase() === operator ? {
983
- tokenId: tokenId.toString(),
984
- vault: a.account || "",
985
- owner: a.owner || "",
986
- agentType: a.agentType || "unknown"
987
- } : null;
1023
+ const [op, opExpires] = await Promise.all([
1024
+ publicClient.readContract({ address: nfaAddr, abi: MY_AGENTS_ABI, functionName: "operatorOf", args: [tokenId] }),
1025
+ publicClient.readContract({ address: nfaAddr, abi: MY_AGENTS_ABI, functionName: "operatorExpiresOf", args: [tokenId] })
1026
+ ]);
1027
+ const isActive = op.toLowerCase() === operator;
1028
+ const now = BigInt(Math.floor(Date.now() / 1e3));
1029
+ const isExpired = !isActive && Number(opExpires) > 0 && now > opExpires;
1030
+ if (isActive) {
1031
+ return {
1032
+ tokenId: tokenId.toString(),
1033
+ vault: a.account || "",
1034
+ owner: a.owner || "",
1035
+ agentType: a.agentType || "unknown",
1036
+ status: "active",
1037
+ operatorExpires: new Date(Number(opExpires) * 1e3).toISOString()
1038
+ };
1039
+ } else if (isExpired) {
1040
+ return {
1041
+ tokenId: tokenId.toString(),
1042
+ vault: a.account || "",
1043
+ owner: a.owner || "",
1044
+ agentType: a.agentType || "unknown",
1045
+ status: "expired",
1046
+ operatorExpires: new Date(Number(opExpires) * 1e3).toISOString(),
1047
+ note: "Operator authorization expired. Renew at https://shll.run/me"
1048
+ };
1049
+ }
1050
+ return null;
988
1051
  } catch {
989
1052
  return null;
990
1053
  }
@@ -1009,6 +1072,8 @@ server.tool(
1009
1072
  async ({ token_id, amount }) => {
1010
1073
  const { policyClient } = createClients();
1011
1074
  const tokenId = BigInt(token_id);
1075
+ const expiryCheck = await checkAgentExpiry(tokenId);
1076
+ if (expiryCheck.expired) return { content: expiryCheck.content };
1012
1077
  const amt = (0, import_viem2.parseEther)(amount);
1013
1078
  const data = (0, import_viem2.encodeFunctionData)({ abi: WBNB_ABI, functionName: "deposit" });
1014
1079
  const action = { target: WBNB, value: amt, data };
@@ -1028,6 +1093,8 @@ server.tool(
1028
1093
  async ({ token_id, amount }) => {
1029
1094
  const { policyClient } = createClients();
1030
1095
  const tokenId = BigInt(token_id);
1096
+ const expiryCheck = await checkAgentExpiry(tokenId);
1097
+ if (expiryCheck.expired) return { content: expiryCheck.content };
1031
1098
  const amt = (0, import_viem2.parseEther)(amount);
1032
1099
  const data = (0, import_viem2.encodeFunctionData)({ abi: WBNB_ABI, functionName: "withdraw", args: [amt] });
1033
1100
  const action = { target: WBNB, value: 0n, data };
@@ -1224,6 +1291,8 @@ server.tool(
1224
1291
  }
1225
1292
  const { account, publicClient, policyClient, config } = createClients();
1226
1293
  const tokenId = BigInt(token_id);
1294
+ const expiryCheck = await checkAgentExpiry(tokenId);
1295
+ if (expiryCheck.expired) return { content: expiryCheck.content };
1227
1296
  const walletClient = (0, import_viem2.createWalletClient)({ account, chain: import_chains2.bsc, transport: (0, import_viem2.http)(config.rpc) });
1228
1297
  const policies = await policyClient.getPolicies(tokenId);
1229
1298
  const results = [];
@@ -1326,6 +1395,8 @@ server.tool(
1326
1395
  async ({ token_id, target, data, value }) => {
1327
1396
  const { policyClient } = createClients();
1328
1397
  const tokenId = BigInt(token_id);
1398
+ const expiryCheck = await checkAgentExpiry(tokenId);
1399
+ if (expiryCheck.expired) return { content: expiryCheck.content };
1329
1400
  const action = {
1330
1401
  target,
1331
1402
  value: BigInt(value),
@@ -1371,6 +1442,8 @@ server.tool(
1371
1442
  async ({ token_id, actions: rawActions }) => {
1372
1443
  const { policyClient } = createClients();
1373
1444
  const tokenId = BigInt(token_id);
1445
+ const expiryCheck = await checkAgentExpiry(tokenId);
1446
+ if (expiryCheck.expired) return { content: expiryCheck.content };
1374
1447
  const actions = rawActions.map((a) => ({
1375
1448
  target: a.target,
1376
1449
  value: BigInt(a.value || "0"),
package/dist/mcp.mjs CHANGED
@@ -152,9 +152,51 @@ function createClients() {
152
152
  });
153
153
  return { account, publicClient, policyClient, config };
154
154
  }
155
+ var AGENT_NFA_EXPIRY_ABI = [
156
+ { name: "operatorExpiresOf", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
157
+ { name: "userExpires", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] }
158
+ ];
159
+ async function checkAgentExpiry(tokenId) {
160
+ const config = getConfig();
161
+ const pc = createPublicClient({ chain: bsc, transport: http(config.rpc) });
162
+ const [operatorExpires, userExpires] = await Promise.all([
163
+ pc.readContract({ address: config.nfa, abi: AGENT_NFA_EXPIRY_ABI, functionName: "operatorExpiresOf", args: [tokenId] }),
164
+ pc.readContract({ address: config.nfa, abi: AGENT_NFA_EXPIRY_ABI, functionName: "userExpires", args: [tokenId] })
165
+ ]);
166
+ const now = BigInt(Math.floor(Date.now() / 1e3));
167
+ if (now > operatorExpires) {
168
+ return {
169
+ expired: true,
170
+ content: [{
171
+ type: "text",
172
+ text: JSON.stringify({
173
+ status: "error",
174
+ message: `Agent token-id ${tokenId} operator authorization has EXPIRED (expired at ${new Date(Number(operatorExpires) * 1e3).toISOString()}). Please renew at https://shll.run/me or use a different token-id.`,
175
+ expiredAt: new Date(Number(operatorExpires) * 1e3).toISOString(),
176
+ action: "renew"
177
+ })
178
+ }]
179
+ };
180
+ }
181
+ if (now > userExpires) {
182
+ return {
183
+ expired: true,
184
+ content: [{
185
+ type: "text",
186
+ text: JSON.stringify({
187
+ status: "error",
188
+ message: `Agent token-id ${tokenId} rental has EXPIRED (expired at ${new Date(Number(userExpires) * 1e3).toISOString()}). Please renew at https://shll.run/me or use a different token-id.`,
189
+ expiredAt: new Date(Number(userExpires) * 1e3).toISOString(),
190
+ action: "renew"
191
+ })
192
+ }]
193
+ };
194
+ }
195
+ return { expired: false };
196
+ }
155
197
  var server = new McpServer({
156
198
  name: "shll-defi",
157
- version: "5.0.0"
199
+ version: "5.2.0"
158
200
  });
159
201
  server.tool(
160
202
  "portfolio",
@@ -252,6 +294,8 @@ server.tool(
252
294
  async ({ token_id, from, to, amount, dex, slippage }) => {
253
295
  const { publicClient, policyClient } = createClients();
254
296
  const tokenId = BigInt(token_id);
297
+ const expiryCheck = await checkAgentExpiry(tokenId);
298
+ if (expiryCheck.expired) return { content: expiryCheck.content };
255
299
  const vault = await policyClient.getVault(tokenId);
256
300
  const fromToken = resolveToken(from);
257
301
  const toToken = resolveToken(to);
@@ -345,6 +389,8 @@ server.tool(
345
389
  async ({ token_id, token, amount }) => {
346
390
  const { publicClient, policyClient } = createClients();
347
391
  const tokenId = BigInt(token_id);
392
+ const expiryCheck = await checkAgentExpiry(tokenId);
393
+ if (expiryCheck.expired) return { content: expiryCheck.content };
348
394
  const symbol = token.toUpperCase();
349
395
  const vTokenAddr = VENUS_VTOKENS[symbol];
350
396
  if (!vTokenAddr) return { content: [{ type: "text", text: JSON.stringify({ error: `Unsupported: ${symbol}. Use: ${Object.keys(VENUS_VTOKENS).join(", ")}` }) }] };
@@ -381,6 +427,8 @@ server.tool(
381
427
  async ({ token_id, token, amount }) => {
382
428
  const { policyClient } = createClients();
383
429
  const tokenId = BigInt(token_id);
430
+ const expiryCheck = await checkAgentExpiry(tokenId);
431
+ if (expiryCheck.expired) return { content: expiryCheck.content };
384
432
  const symbol = token.toUpperCase();
385
433
  const vTokenAddr = VENUS_VTOKENS[symbol];
386
434
  if (!vTokenAddr) return { content: [{ type: "text", text: JSON.stringify({ error: `Unsupported: ${symbol}` }) }] };
@@ -437,6 +485,8 @@ server.tool(
437
485
  async ({ token_id, token, amount, to }) => {
438
486
  const { policyClient } = createClients();
439
487
  const tokenId = BigInt(token_id);
488
+ const expiryCheck = await checkAgentExpiry(tokenId);
489
+ if (expiryCheck.expired) return { content: expiryCheck.content };
440
490
  const tokenInfo = resolveToken(token);
441
491
  const amt = parseAmount(amount, tokenInfo.decimals);
442
492
  const recipient = to;
@@ -457,17 +507,14 @@ server.tool(
457
507
  return { content: [{ type: "text", text: JSON.stringify({ status: "success", hash: result.hash, token, amount, to: recipient }) }] };
458
508
  }
459
509
  );
460
- var OPERATOR_OF_ABI = [{
461
- type: "function",
462
- name: "operatorOf",
463
- inputs: [{ name: "tokenId", type: "uint256" }],
464
- outputs: [{ name: "", type: "address" }],
465
- stateMutability: "view"
466
- }];
510
+ var MY_AGENTS_ABI = [
511
+ { type: "function", name: "operatorOf", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }], stateMutability: "view" },
512
+ { type: "function", name: "operatorExpiresOf", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }], stateMutability: "view" }
513
+ ];
467
514
  var DEFAULT_INDEXER = "https://indexer-mainnet.shll.run";
468
515
  server.tool(
469
516
  "my_agents",
470
- "List all agents where the current operator key is authorized. Returns token IDs, vault addresses, and agent types. Call this first if the user does not specify a token ID.",
517
+ "List all agents where the current operator key is or was authorized. Returns active agents and expired agents that need renewal.",
471
518
  {},
472
519
  async () => {
473
520
  const { account, publicClient, config } = createClients();
@@ -484,18 +531,34 @@ server.tool(
484
531
  agents.map(async (a) => {
485
532
  const tokenId = BigInt(a.tokenId);
486
533
  try {
487
- const op = await publicClient.readContract({
488
- address: nfaAddr,
489
- abi: OPERATOR_OF_ABI,
490
- functionName: "operatorOf",
491
- args: [tokenId]
492
- });
493
- return op.toLowerCase() === operator ? {
494
- tokenId: tokenId.toString(),
495
- vault: a.account || "",
496
- owner: a.owner || "",
497
- agentType: a.agentType || "unknown"
498
- } : null;
534
+ const [op, opExpires] = await Promise.all([
535
+ publicClient.readContract({ address: nfaAddr, abi: MY_AGENTS_ABI, functionName: "operatorOf", args: [tokenId] }),
536
+ publicClient.readContract({ address: nfaAddr, abi: MY_AGENTS_ABI, functionName: "operatorExpiresOf", args: [tokenId] })
537
+ ]);
538
+ const isActive = op.toLowerCase() === operator;
539
+ const now = BigInt(Math.floor(Date.now() / 1e3));
540
+ const isExpired = !isActive && Number(opExpires) > 0 && now > opExpires;
541
+ if (isActive) {
542
+ return {
543
+ tokenId: tokenId.toString(),
544
+ vault: a.account || "",
545
+ owner: a.owner || "",
546
+ agentType: a.agentType || "unknown",
547
+ status: "active",
548
+ operatorExpires: new Date(Number(opExpires) * 1e3).toISOString()
549
+ };
550
+ } else if (isExpired) {
551
+ return {
552
+ tokenId: tokenId.toString(),
553
+ vault: a.account || "",
554
+ owner: a.owner || "",
555
+ agentType: a.agentType || "unknown",
556
+ status: "expired",
557
+ operatorExpires: new Date(Number(opExpires) * 1e3).toISOString(),
558
+ note: "Operator authorization expired. Renew at https://shll.run/me"
559
+ };
560
+ }
561
+ return null;
499
562
  } catch {
500
563
  return null;
501
564
  }
@@ -520,6 +583,8 @@ server.tool(
520
583
  async ({ token_id, amount }) => {
521
584
  const { policyClient } = createClients();
522
585
  const tokenId = BigInt(token_id);
586
+ const expiryCheck = await checkAgentExpiry(tokenId);
587
+ if (expiryCheck.expired) return { content: expiryCheck.content };
523
588
  const amt = parseEther(amount);
524
589
  const data = encodeFunctionData({ abi: WBNB_ABI, functionName: "deposit" });
525
590
  const action = { target: WBNB, value: amt, data };
@@ -539,6 +604,8 @@ server.tool(
539
604
  async ({ token_id, amount }) => {
540
605
  const { policyClient } = createClients();
541
606
  const tokenId = BigInt(token_id);
607
+ const expiryCheck = await checkAgentExpiry(tokenId);
608
+ if (expiryCheck.expired) return { content: expiryCheck.content };
542
609
  const amt = parseEther(amount);
543
610
  const data = encodeFunctionData({ abi: WBNB_ABI, functionName: "withdraw", args: [amt] });
544
611
  const action = { target: WBNB, value: 0n, data };
@@ -735,6 +802,8 @@ server.tool(
735
802
  }
736
803
  const { account, publicClient, policyClient, config } = createClients();
737
804
  const tokenId = BigInt(token_id);
805
+ const expiryCheck = await checkAgentExpiry(tokenId);
806
+ if (expiryCheck.expired) return { content: expiryCheck.content };
738
807
  const walletClient = createWalletClient({ account, chain: bsc, transport: http(config.rpc) });
739
808
  const policies = await policyClient.getPolicies(tokenId);
740
809
  const results = [];
@@ -837,6 +906,8 @@ server.tool(
837
906
  async ({ token_id, target, data, value }) => {
838
907
  const { policyClient } = createClients();
839
908
  const tokenId = BigInt(token_id);
909
+ const expiryCheck = await checkAgentExpiry(tokenId);
910
+ if (expiryCheck.expired) return { content: expiryCheck.content };
840
911
  const action = {
841
912
  target,
842
913
  value: BigInt(value),
@@ -882,6 +953,8 @@ server.tool(
882
953
  async ({ token_id, actions: rawActions }) => {
883
954
  const { policyClient } = createClients();
884
955
  const tokenId = BigInt(token_id);
956
+ const expiryCheck = await checkAgentExpiry(tokenId);
957
+ if (expiryCheck.expired) return { content: expiryCheck.content };
885
958
  const actions = rawActions.map((a) => ({
886
959
  target: a.target,
887
960
  value: BigInt(a.value || "0"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shll-skills",
3
- "version": "5.1.1",
3
+ "version": "5.2.1",
4
4
  "description": "SHLL DeFi Agent — CLI + MCP Server for BSC",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/mcp.ts CHANGED
@@ -191,13 +191,56 @@ function createClients() {
191
191
  return { account, publicClient, policyClient, config };
192
192
  }
193
193
 
194
+ // Expiry pre-check: prevents write operations on expired agents with clear error
195
+ const AGENT_NFA_EXPIRY_ABI = [
196
+ { name: "operatorExpiresOf", type: "function" as const, stateMutability: "view" as const, inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
197
+ { name: "userExpires", type: "function" as const, stateMutability: "view" as const, inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }] },
198
+ ] as const;
199
+
200
+ async function checkAgentExpiry(tokenId: bigint) {
201
+ const config = getConfig();
202
+ const pc = createPublicClient({ chain: bsc, transport: http(config.rpc) });
203
+ const [operatorExpires, userExpires] = await Promise.all([
204
+ pc.readContract({ address: config.nfa as Address, abi: AGENT_NFA_EXPIRY_ABI, functionName: "operatorExpiresOf", args: [tokenId] }) as Promise<bigint>,
205
+ pc.readContract({ address: config.nfa as Address, abi: AGENT_NFA_EXPIRY_ABI, functionName: "userExpires", args: [tokenId] }) as Promise<bigint>,
206
+ ]);
207
+ const now = BigInt(Math.floor(Date.now() / 1000));
208
+ if (now > operatorExpires) {
209
+ return {
210
+ expired: true,
211
+ content: [{
212
+ type: "text" as const, text: JSON.stringify({
213
+ status: "error",
214
+ message: `Agent token-id ${tokenId} operator authorization has EXPIRED (expired at ${new Date(Number(operatorExpires) * 1000).toISOString()}). Please renew at https://shll.run/me or use a different token-id.`,
215
+ expiredAt: new Date(Number(operatorExpires) * 1000).toISOString(),
216
+ action: "renew",
217
+ })
218
+ }],
219
+ };
220
+ }
221
+ if (now > userExpires) {
222
+ return {
223
+ expired: true,
224
+ content: [{
225
+ type: "text" as const, text: JSON.stringify({
226
+ status: "error",
227
+ message: `Agent token-id ${tokenId} rental has EXPIRED (expired at ${new Date(Number(userExpires) * 1000).toISOString()}). Please renew at https://shll.run/me or use a different token-id.`,
228
+ expiredAt: new Date(Number(userExpires) * 1000).toISOString(),
229
+ action: "renew",
230
+ })
231
+ }],
232
+ };
233
+ }
234
+ return { expired: false };
235
+ }
236
+
194
237
  // ═══════════════════════════════════════════════════════
195
238
  // MCP Server
196
239
  // ═══════════════════════════════════════════════════════
197
240
 
198
241
  const server = new McpServer({
199
242
  name: "shll-defi",
200
- version: "5.0.0",
243
+ version: "5.2.0",
201
244
  });
202
245
 
203
246
  // ── Tool: portfolio ─────────────────────────────────────
@@ -306,6 +349,7 @@ server.tool(
306
349
  async ({ token_id, from, to, amount, dex, slippage }) => {
307
350
  const { publicClient, policyClient } = createClients();
308
351
  const tokenId = BigInt(token_id);
352
+ const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
309
353
  const vault = await policyClient.getVault(tokenId);
310
354
 
311
355
  const fromToken = resolveToken(from);
@@ -410,6 +454,7 @@ server.tool(
410
454
  async ({ token_id, token, amount }) => {
411
455
  const { publicClient, policyClient } = createClients();
412
456
  const tokenId = BigInt(token_id);
457
+ const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
413
458
  const symbol = token.toUpperCase();
414
459
  const vTokenAddr = VENUS_VTOKENS[symbol];
415
460
  if (!vTokenAddr) return { content: [{ type: "text" as const, text: JSON.stringify({ error: `Unsupported: ${symbol}. Use: ${Object.keys(VENUS_VTOKENS).join(", ")}` }) }] };
@@ -455,6 +500,7 @@ server.tool(
455
500
  async ({ token_id, token, amount }) => {
456
501
  const { policyClient } = createClients();
457
502
  const tokenId = BigInt(token_id);
503
+ const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
458
504
  const symbol = token.toUpperCase();
459
505
  const vTokenAddr = VENUS_VTOKENS[symbol];
460
506
  if (!vTokenAddr) return { content: [{ type: "text" as const, text: JSON.stringify({ error: `Unsupported: ${symbol}` }) }] };
@@ -519,6 +565,7 @@ server.tool(
519
565
  async ({ token_id, token, amount, to }) => {
520
566
  const { policyClient } = createClients();
521
567
  const tokenId = BigInt(token_id);
568
+ const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
522
569
  const tokenInfo = resolveToken(token);
523
570
  const amt = parseAmount(amount, tokenInfo.decimals);
524
571
  const recipient = to as Address;
@@ -546,18 +593,16 @@ server.tool(
546
593
  );
547
594
 
548
595
  // ── Tool: my_agents ─────────────────────────────────────
549
- const OPERATOR_OF_ABI = [{
550
- type: "function" as const, name: "operatorOf",
551
- inputs: [{ name: "tokenId", type: "uint256" }],
552
- outputs: [{ name: "", type: "address" }],
553
- stateMutability: "view" as const,
554
- }] as const;
596
+ const MY_AGENTS_ABI = [
597
+ { type: "function" as const, name: "operatorOf", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "address" }], stateMutability: "view" as const },
598
+ { type: "function" as const, name: "operatorExpiresOf", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ name: "", type: "uint256" }], stateMutability: "view" as const },
599
+ ] as const;
555
600
 
556
601
  const DEFAULT_INDEXER = "https://indexer-mainnet.shll.run";
557
602
 
558
603
  server.tool(
559
604
  "my_agents",
560
- "List all agents where the current operator key is authorized. Returns token IDs, vault addresses, and agent types. Call this first if the user does not specify a token ID.",
605
+ "List all agents where the current operator key is or was authorized. Returns active agents and expired agents that need renewal.",
561
606
  {},
562
607
  async () => {
563
608
  const { account, publicClient, config } = createClients();
@@ -574,23 +619,34 @@ server.tool(
574
619
  return { content: [{ type: "text" as const, text: JSON.stringify({ operator, agents: [], count: 0 }) }] };
575
620
  }
576
621
 
577
- // 2. Batch check operatorOf for all agents
622
+ // 2. Check operatorOf AND operatorExpiresOf for all agents
578
623
  const checks = await Promise.all(
579
624
  agents.map(async (a) => {
580
625
  const tokenId = BigInt(a.tokenId!);
581
626
  try {
582
- const op = await publicClient.readContract({
583
- address: nfaAddr,
584
- abi: OPERATOR_OF_ABI,
585
- functionName: "operatorOf",
586
- args: [tokenId],
587
- });
588
- return (op as string).toLowerCase() === operator ? {
589
- tokenId: tokenId.toString(),
590
- vault: a.account || "",
591
- owner: a.owner || "",
592
- agentType: a.agentType || "unknown",
593
- } : null;
627
+ const [op, opExpires] = await Promise.all([
628
+ publicClient.readContract({ address: nfaAddr, abi: MY_AGENTS_ABI, functionName: "operatorOf", args: [tokenId] }) as Promise<string>,
629
+ publicClient.readContract({ address: nfaAddr, abi: MY_AGENTS_ABI, functionName: "operatorExpiresOf", args: [tokenId] }) as Promise<bigint>,
630
+ ]);
631
+ const isActive = op.toLowerCase() === operator;
632
+ const now = BigInt(Math.floor(Date.now() / 1000));
633
+ const isExpired = !isActive && Number(opExpires) > 0 && now > opExpires;
634
+
635
+ if (isActive) {
636
+ return {
637
+ tokenId: tokenId.toString(), vault: a.account || "", owner: a.owner || "",
638
+ agentType: a.agentType || "unknown", status: "active" as const,
639
+ operatorExpires: new Date(Number(opExpires) * 1000).toISOString(),
640
+ };
641
+ } else if (isExpired) {
642
+ return {
643
+ tokenId: tokenId.toString(), vault: a.account || "", owner: a.owner || "",
644
+ agentType: a.agentType || "unknown", status: "expired" as const,
645
+ operatorExpires: new Date(Number(opExpires) * 1000).toISOString(),
646
+ note: "Operator authorization expired. Renew at https://shll.run/me",
647
+ };
648
+ }
649
+ return null;
594
650
  } catch { return null; }
595
651
  })
596
652
  );
@@ -616,6 +672,7 @@ server.tool(
616
672
  async ({ token_id, amount }) => {
617
673
  const { policyClient } = createClients();
618
674
  const tokenId = BigInt(token_id);
675
+ const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
619
676
  const amt = parseEther(amount);
620
677
  const data = encodeFunctionData({ abi: WBNB_ABI, functionName: "deposit" });
621
678
  const action: Action = { target: WBNB as Address, value: amt, data };
@@ -639,6 +696,7 @@ server.tool(
639
696
  async ({ token_id, amount }) => {
640
697
  const { policyClient } = createClients();
641
698
  const tokenId = BigInt(token_id);
699
+ const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
642
700
  const amt = parseEther(amount);
643
701
  const data = encodeFunctionData({ abi: WBNB_ABI, functionName: "withdraw", args: [amt] });
644
702
  const action: Action = { target: WBNB as Address, value: 0n, data };
@@ -862,6 +920,7 @@ server.tool(
862
920
 
863
921
  const { account, publicClient, policyClient, config } = createClients();
864
922
  const tokenId = BigInt(token_id);
923
+ const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
865
924
  const walletClient = createWalletClient({ account, chain: bsc, transport: http(config.rpc) });
866
925
  const policies = await policyClient.getPolicies(tokenId);
867
926
  const results: string[] = [];
@@ -983,6 +1042,7 @@ server.tool(
983
1042
  async ({ token_id, target, data, value }) => {
984
1043
  const { policyClient } = createClients();
985
1044
  const tokenId = BigInt(token_id);
1045
+ const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
986
1046
  const action: Action = {
987
1047
  target: target as Address,
988
1048
  value: BigInt(value),
@@ -1031,6 +1091,7 @@ server.tool(
1031
1091
  async ({ token_id, actions: rawActions }) => {
1032
1092
  const { policyClient } = createClients();
1033
1093
  const tokenId = BigInt(token_id);
1094
+ const expiryCheck = await checkAgentExpiry(tokenId); if (expiryCheck.expired) return { content: expiryCheck.content! };
1034
1095
  const actions: Action[] = rawActions.map(a => ({
1035
1096
  target: a.target as Address,
1036
1097
  value: BigInt(a.value || "0"),