@champz-llc/legends-mcp-server 1.2.1 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +391 -4
  2. package/package.json +2 -3
package/index.js CHANGED
@@ -8,6 +8,27 @@ import {
8
8
  } from '@modelcontextprotocol/sdk/types.js';
9
9
  import fetch from 'node-fetch';
10
10
 
11
+ // Helper function to fetch image and convert to base64
12
+ async function fetchImageAsBase64(imageUrl) {
13
+ try {
14
+ const response = await fetch(imageUrl);
15
+ if (!response.ok) {
16
+ throw new Error(`Failed to fetch image: ${response.status}`);
17
+ }
18
+ const buffer = await response.buffer();
19
+ return buffer.toString('base64');
20
+ } catch (error) {
21
+ console.error(`Error fetching image ${imageUrl}:`, error.message);
22
+ return null;
23
+ }
24
+ }
25
+
26
+ // MCP Signature Authentication (optional - for player data access)
27
+ const WALLET = process.env.WALLET;
28
+ const SIGNATURE = process.env.SIGNATURE;
29
+ const SIGNED_AT = process.env.SIGNED_AT;
30
+ const hasWalletAuth = WALLET && SIGNATURE && SIGNED_AT;
31
+
11
32
  // Hardcoded rewards data for demo
12
33
  const DEMO_WALLET = '0xfbc159e35f56580d5d297af18a8c19f83d66088a';
13
34
 
@@ -100,8 +121,7 @@ const server = new Server(
100
121
 
101
122
  // List available tools
102
123
  server.setRequestHandler(ListToolsRequestSchema, async () => {
103
- return {
104
- tools: [
124
+ const tools = [
105
125
  {
106
126
  name: 'legends_global_stats',
107
127
  description: 'Get overall Legends of Champz game statistics including total legends rolled, CHAMPZ burned, USDC distributed, total battles, and more',
@@ -201,8 +221,52 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
201
221
  required: []
202
222
  },
203
223
  }
204
- ],
205
- };
224
+ ];
225
+
226
+ // Add player data tools if wallet is configured
227
+ if (hasWalletAuth) {
228
+ tools.push({
229
+ name: 'legends_player_data',
230
+ description: 'Get your personal Legends of Champz statistics including CHAMPZ spent on packs, legends owned, thrones, battle stats, claims history, and saved trainers. Requires wallet authentication.',
231
+ inputSchema: {
232
+ type: 'object',
233
+ properties: {},
234
+ required: []
235
+ },
236
+ });
237
+
238
+ tools.push({
239
+ name: 'show_legend',
240
+ description: 'Show details and image for a specific legend you own by ID. Use this when the user asks to see a particular legend (e.g., "show me legend #1010").',
241
+ inputSchema: {
242
+ type: 'object',
243
+ properties: {
244
+ legend_id: {
245
+ type: 'number',
246
+ description: 'The ID of the legend to display'
247
+ }
248
+ },
249
+ required: ['legend_id']
250
+ },
251
+ });
252
+
253
+ tools.push({
254
+ name: 'show_throne',
255
+ description: 'Show details and image for a specific throne you own by ID. Use this when the user asks to see a particular throne.',
256
+ inputSchema: {
257
+ type: 'object',
258
+ properties: {
259
+ throne_id: {
260
+ type: 'number',
261
+ description: 'The ID of the throne to display'
262
+ }
263
+ },
264
+ required: ['throne_id']
265
+ },
266
+ });
267
+ }
268
+
269
+ return { tools };
206
270
  });
207
271
 
208
272
  // Handle tool calls
@@ -531,6 +595,329 @@ Last updated: ${new Date(stats.cached_at * 1000).toLocaleString()}`;
531
595
  };
532
596
  }
533
597
 
598
+ case 'legends_player_data':
599
+ // Check if wallet authentication is configured
600
+ if (!hasWalletAuth) {
601
+ return {
602
+ content: [
603
+ {
604
+ type: 'text',
605
+ text: `To access your personal Legends of Champz data, you need to connect your wallet.
606
+
607
+ 🔗 Setup Guide: https://legends.champz.world/mcp-setup
608
+
609
+ This one-time setup takes 2 minutes:
610
+ 1. Visit the link above on your desktop
611
+ 2. Connect your Coinbase Wallet
612
+ 3. Sign a message to prove ownership
613
+ 4. Copy the config to Claude Desktop
614
+ 5. Restart Claude Desktop
615
+
616
+ You can still ask about:
617
+ • Global game stats (legends_global_stats)
618
+ • Leaderboards (legends_leaderboard_*)
619
+ • Current cycle information`,
620
+ },
621
+ ],
622
+ };
623
+ }
624
+
625
+ try {
626
+ // Call player-data endpoint with signature authentication
627
+ const url = `https://api.champz.world/game/spore-trainer/player-data?wallet=${encodeURIComponent(WALLET)}&signature=${encodeURIComponent(SIGNATURE)}&timestamp=${encodeURIComponent(SIGNED_AT)}`;
628
+ const response = await fetch(url);
629
+ const data = await response.json();
630
+
631
+ if (!data.success) {
632
+ throw new Error(data.error || 'Failed to fetch player data');
633
+ }
634
+
635
+ // Format player data
636
+ const displayName = data.display_name || data.basename || `${data.wallet.slice(0, 8)}...`;
637
+ const stats = data.statistics;
638
+ const claims = data.claims;
639
+
640
+ let output = `Legends of Champz - Your Statistics\n\n`;
641
+ output += `Player: ${displayName}\n`;
642
+ output += `Wallet: ${data.wallet}\n`;
643
+ if (data.basename) {
644
+ output += `Basename: ${data.basename}\n`;
645
+ }
646
+ output += `Member since: ${new Date(stats.member_since).toLocaleDateString()}\n\n`;
647
+
648
+ output += `📦 Packs & Legends:\n`;
649
+ output += ` • Total packs opened: ${stats.total_packs_opened}\n`;
650
+ output += ` • CHAMPZ spent on packs: ${stats.champz_spent_on_packs.toLocaleString()}\n`;
651
+ output += ` • Legends owned: ${stats.legends_owned}\n`;
652
+ output += ` • Saved trainer slots: ${stats.saved_trainer_slots}\n\n`;
653
+
654
+ output += `⚔️ Battle Statistics:\n`;
655
+ output += ` • Total battles: ${stats.total_battles}\n`;
656
+ output += ` • Wins: ${stats.battles_won} (${(stats.win_rate * 100).toFixed(1)}% win rate)\n`;
657
+ output += ` • Losses: ${stats.battles_lost}\n`;
658
+ output += ` • Current streak: ${stats.current_win_streak}\n`;
659
+ output += ` • Best streak: ${stats.best_win_streak}\n\n`;
660
+
661
+ output += `👑 Thrones & Guardian:\n`;
662
+ output += ` • Thrones owned: ${stats.thrones_owned}\n`;
663
+ output += ` • Times held guardian: ${stats.times_held_guardian}\n`;
664
+ output += ` • CHAMPZ spent on guardian: ${stats.champz_spent_on_guardian.toLocaleString()}\n\n`;
665
+
666
+ output += `💰 Claims History:\n`;
667
+ output += ` • Total USDC claimed: $${claims.total_usdc_claimed}\n`;
668
+ output += ` • Total CHAMPZ claimed: ${claims.total_champz_claimed.toLocaleString()}\n`;
669
+ output += ` • Battle USDC (type 6): ${claims.by_type['6'].count} claims, $${claims.by_type['6'].total_amount}\n`;
670
+ output += ` • Battle CHAMPZ (type 5): ${claims.by_type['5'].count} claims, ${claims.by_type['5'].total_amount.toLocaleString()} tokens\n`;
671
+ output += ` • Guardian USDC (type 19): ${claims.by_type['19'].count} claims, $${claims.by_type['19'].total_amount}\n`;
672
+ output += ` • Guardian CHAMPZ (type 18): ${claims.by_type['18'].count} claims, ${claims.by_type['18'].total_amount.toLocaleString()} tokens\n\n`;
673
+
674
+ // Build content array with text and images
675
+ const content = [
676
+ {
677
+ type: 'text',
678
+ text: output,
679
+ },
680
+ ];
681
+
682
+ // Add throne images (show all thrones)
683
+ if (data.thrones && data.thrones.length > 0) {
684
+ const throneText = `\n🏆 Throne Collection (${data.thrones.length}):\n`;
685
+ let throneList = '';
686
+
687
+ for (const [i, throne] of data.thrones.entries()) {
688
+ throneList += `${i + 1}. ${throne.name} (${throne.rarity.toUpperCase()}) - Cycle ${throne.cycle_id}\n`;
689
+
690
+ // Add throne image
691
+ const imageUrl = `https://img.champz.world${throne.image_path}`;
692
+ const base64Data = await fetchImageAsBase64(imageUrl);
693
+ if (base64Data) {
694
+ content.push({
695
+ type: 'image',
696
+ data: base64Data,
697
+ mimeType: 'image/png',
698
+ });
699
+ }
700
+ }
701
+
702
+ content[0].text += throneText + throneList + '\n';
703
+ }
704
+
705
+ // Show legend summary (don't show all images by default - too many)
706
+ if (data.all_legends && data.all_legends.length > 0) {
707
+ const legendText = `\n🍄 All Legends Owned (${data.all_legends.length}):\n`;
708
+ let legendSummary = '';
709
+
710
+ // Group by rarity
711
+ const byRarity = {
712
+ unique: [],
713
+ legendary: [],
714
+ epic: [],
715
+ rare: [],
716
+ common: []
717
+ };
718
+
719
+ data.all_legends.forEach(legend => {
720
+ byRarity[legend.rarity]?.push(legend);
721
+ });
722
+
723
+ Object.entries(byRarity).forEach(([rarity, legends]) => {
724
+ if (legends.length > 0) {
725
+ legendSummary += ` ${rarity.toUpperCase()}: ${legends.length} legends\n`;
726
+ }
727
+ });
728
+
729
+ legendSummary += `\nTo view a specific legend, ask: "Show me legend #<ID>"\n`;
730
+ legendSummary += `Example: "Show me legend #1010"\n\n`;
731
+
732
+ content[0].text += legendText + legendSummary;
733
+ }
734
+
735
+ // Add saved trainer images (premium collection)
736
+ if (data.saved_trainers && data.saved_trainers.length > 0) {
737
+ const trainerText = `⭐ Saved Trainers (${data.saved_trainers.length}):\n`;
738
+ let trainerList = '';
739
+
740
+ for (const [i, trainer] of data.saved_trainers.entries()) {
741
+ trainerList += `${i + 1}. ${trainer.rarity.toUpperCase()} Legend #${trainer.legend_id} - Power: ${trainer.total_power}\n`;
742
+ trainerList += ` ATK: ${trainer.attack} | DEF: ${trainer.defense} | SPD: ${trainer.speed}\n`;
743
+ trainerList += ` Elements: ${trainer.elements.join(', ')}\n\n`;
744
+
745
+ // Add trainer image
746
+ const imageUrl = `https://img.champz.world${trainer.image_path}`;
747
+ const base64Data = await fetchImageAsBase64(imageUrl);
748
+ if (base64Data) {
749
+ content.push({
750
+ type: 'image',
751
+ data: base64Data,
752
+ mimeType: 'image/png',
753
+ });
754
+ }
755
+ }
756
+
757
+ content[0].text += trainerText + trainerList;
758
+ }
759
+
760
+ return { content };
761
+ } catch (error) {
762
+ return {
763
+ content: [
764
+ {
765
+ type: 'text',
766
+ text: `Error fetching player data: ${error.message}\n\nMake sure your wallet is registered in Legends of Champz. Visit https://legends.champz.world to play!`,
767
+ },
768
+ ],
769
+ };
770
+ }
771
+
772
+ case 'show_legend':
773
+ if (!hasWalletAuth) {
774
+ return {
775
+ content: [{ type: 'text', text: 'Authentication required. Visit https://legends.champz.world/mcp-setup' }],
776
+ };
777
+ }
778
+
779
+ try {
780
+ const legendId = request.params.arguments?.legend_id;
781
+ if (!legendId) {
782
+ throw new Error('legend_id is required');
783
+ }
784
+
785
+ // Fetch player data
786
+ const url = `https://api.champz.world/game/spore-trainer/player-data?wallet=${encodeURIComponent(WALLET)}&signature=${encodeURIComponent(SIGNATURE)}&timestamp=${encodeURIComponent(SIGNED_AT)}`;
787
+ const response = await fetch(url);
788
+ const data = await response.json();
789
+
790
+ if (!data.success) {
791
+ throw new Error('Failed to fetch player data');
792
+ }
793
+
794
+ // Find the legend in all_legends
795
+ const legend = data.all_legends?.find(l => l.legend_id === legendId);
796
+
797
+ if (!legend) {
798
+ return {
799
+ content: [
800
+ {
801
+ type: 'text',
802
+ text: `Legend #${legendId} not found in your collection. You own ${data.all_legends?.length || 0} legends total.`,
803
+ },
804
+ ],
805
+ };
806
+ }
807
+
808
+ // Build output
809
+ let output = `Legend #${legend.legend_id} - ${legend.name}\n\n`;
810
+ output += `Rarity: ${legend.rarity.toUpperCase()}\n`;
811
+ output += `Total Power: ${legend.total_power}\n`;
812
+ output += `ATK: ${legend.attack} | DEF: ${legend.defense} | SPD: ${legend.speed}\n`;
813
+ output += `Elements: ${legend.elements.join(', ')}\n`;
814
+ output += `Rolled: ${new Date(legend.rolled_at).toLocaleDateString()}\n`;
815
+ output += `Saved: ${legend.is_saved ? 'Yes ⭐' : 'No'}\n`;
816
+
817
+ // Fetch and convert image to base64
818
+ const imageUrl = `https://img.champz.world${legend.image_path}`;
819
+ const base64Data = await fetchImageAsBase64(imageUrl);
820
+
821
+ const content = [
822
+ {
823
+ type: 'text',
824
+ text: output,
825
+ },
826
+ ];
827
+
828
+ if (base64Data) {
829
+ content.push({
830
+ type: 'image',
831
+ data: base64Data,
832
+ mimeType: 'image/png',
833
+ });
834
+ }
835
+
836
+ return { content };
837
+ } catch (error) {
838
+ return {
839
+ content: [
840
+ {
841
+ type: 'text',
842
+ text: `Error showing legend: ${error.message}`,
843
+ },
844
+ ],
845
+ };
846
+ }
847
+
848
+ case 'show_throne':
849
+ if (!hasWalletAuth) {
850
+ return {
851
+ content: [{ type: 'text', text: 'Authentication required. Visit https://legends.champz.world/mcp-setup' }],
852
+ };
853
+ }
854
+
855
+ try {
856
+ const throneId = request.params.arguments?.throne_id;
857
+ if (!throneId) {
858
+ throw new Error('throne_id is required');
859
+ }
860
+
861
+ // Fetch player data
862
+ const url = `https://api.champz.world/game/spore-trainer/player-data?wallet=${encodeURIComponent(WALLET)}&signature=${encodeURIComponent(SIGNATURE)}&timestamp=${encodeURIComponent(SIGNED_AT)}`;
863
+ const response = await fetch(url);
864
+ const data = await response.json();
865
+
866
+ if (!data.success) {
867
+ throw new Error('Failed to fetch player data');
868
+ }
869
+
870
+ // Find the throne
871
+ const throne = data.thrones?.find(t => t.throne_id === throneId);
872
+
873
+ if (!throne) {
874
+ return {
875
+ content: [
876
+ {
877
+ type: 'text',
878
+ text: `Throne #${throneId} not found in your collection. You own ${data.thrones?.length || 0} thrones total.`,
879
+ },
880
+ ],
881
+ };
882
+ }
883
+
884
+ // Build output
885
+ let output = `Throne #${throne.throne_id} - ${throne.name}\n\n`;
886
+ output += `Rarity: ${throne.rarity.toUpperCase()}\n`;
887
+ output += `Earned in Cycle: ${throne.cycle_id}\n`;
888
+ output += `Earned: ${new Date(throne.earned_at).toLocaleDateString()}\n`;
889
+
890
+ // Fetch and convert image to base64
891
+ const imageUrl = `https://img.champz.world${throne.image_path}`;
892
+ const base64Data = await fetchImageAsBase64(imageUrl);
893
+
894
+ const content = [
895
+ {
896
+ type: 'text',
897
+ text: output,
898
+ },
899
+ ];
900
+
901
+ if (base64Data) {
902
+ content.push({
903
+ type: 'image',
904
+ data: base64Data,
905
+ mimeType: 'image/png',
906
+ });
907
+ }
908
+
909
+ return { content };
910
+ } catch (error) {
911
+ return {
912
+ content: [
913
+ {
914
+ type: 'text',
915
+ text: `Error showing throne: ${error.message}`,
916
+ },
917
+ ],
918
+ };
919
+ }
920
+
534
921
  default:
535
922
  throw new Error(`Unknown tool: ${name}`);
536
923
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@champz-llc/legends-mcp-server",
3
- "version": "1.2.1",
4
- "description": "MCP server for Legends of Champz - Query game stats and claim rewards through Claude Desktop",
3
+ "version": "1.3.4",
4
+ "description": "MCP server for Legends of Champz - Query game stats, access personal data with signature auth, and claim rewards through Claude Desktop",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "bin": {
@@ -33,7 +33,6 @@
33
33
  "node": ">=18.0.0"
34
34
  },
35
35
  "dependencies": {
36
- "@champz-llc/legends-mcp-server": "^1.1.0",
37
36
  "@modelcontextprotocol/sdk": "^1.0.0",
38
37
  "node-fetch": "^3.3.2"
39
38
  }