earth2-mcp-server
Version:
Earth2 MCP Server for Claude integration - Access your Earth2 account data through Claude
758 lines (693 loc) • 24.6 kB
JavaScript
#!/usr/bin/env node
/**
* Earth2 MCP Server for Claude Integration
* Provides access to Earth2 account data via Claude
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from "@modelcontextprotocol/sdk/types.js";
import { Earth2Client } from "./src/earth2-client.mjs";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
import dotenv from "dotenv";
// Load environment variables
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
dotenv.config({ path: join(__dirname, ".env") });
class Earth2MCPServer {
constructor() {
this.server = new Server(
{
name: "earth2",
version: "1.3.0",
description: "Earth2 Integration - Access your Earth2 account data through Claude.\n\n**IMPORTANT - Link Formatting:**\nWhen mentioning properties or users, ALWAYS include clickable links:\n- Property links: https://app.earth2.io/#propertyInfo/{landfield_id} (use ID from response)\n- User profile links: https://app.earth2.io/#profile/{user_id} (use ID from response)\nExample: 'Check out this [property](https://app.earth2.io/#propertyInfo/5e1b65f2-15b0-4eb6-bf01-063bd0a843fc)'\nAlways use actual IDs from API responses, never make them up.",
},
{
capabilities: {
tools: {},
},
}
);
this.earth2Client = null;
this.setupToolHandlers();
this.setupErrorHandling();
}
setupErrorHandling() {
this.server.onerror = (error) => {
console.error("[MCP Error]", error);
};
process.on("SIGINT", async () => {
await this.server.close();
process.exit(0);
});
}
/**
* Initialize Earth2 client with credentials from environment
* Supports both user credentials and fallback to Earthie credentials
*/
initializeClient() {
// Try user credentials first
let xsrfToken = process.env.EARTH2_XSRF_TOKEN;
let cookie = process.env.EARTH2_COOKIE;
// Fallback to Earthie/Platform credentials if user credentials not available
if (!xsrfToken || !cookie) {
xsrfToken = process.env.EARTHIE_XSRF_TOKEN || process.env.HARDCODED_CSRF;
cookie = process.env.EARTHIE_COOKIE || process.env.HARDCODED_COOKIE;
}
if (!xsrfToken || !cookie) {
throw new McpError(
ErrorCode.InvalidRequest,
"Earth2 credentials not found. Please set either (EARTH2_XSRF_TOKEN and EARTH2_COOKIE) for user credentials, or (EARTHIE_XSRF_TOKEN and EARTHIE_COOKIE) for platform credentials."
);
}
this.earth2Client = new Earth2Client(xsrfToken, cookie);
// Log which credentials are being used (only in debug mode)
if (process.env.MCP_DEBUG) {
const credType = process.env.EARTH2_XSRF_TOKEN ? "user" : "earthie/platform";
console.error(`[MCP] Using ${credType} credentials`);
}
}
setupToolHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
// User Profile & Info
{
name: "get_user_info",
description: "Get Earth2 user profile information for any user",
inputSchema: {
type: "object",
properties: {
user_id: {
type: "string",
description: "User ID (optional, defaults to current user)",
},
},
},
},
{
name: "get_my_networth",
description: "Get your current Earth2 net worth",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "search_users",
description: "Search for Earth2 users by username",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query",
},
},
required: ["query"],
},
},
// Properties / Landfields
{
name: "get_landfields",
description: "Get all your Earth2 properties/landfields with optional filters",
inputSchema: {
type: "object",
properties: {
country: {
type: "string",
description: "Filter by country code",
},
minTiles: {
type: "number",
description: "Minimum number of tiles",
},
hasHolobuilding: {
type: "boolean",
description: "Filter properties with holobuildings",
},
},
},
},
{
name: "get_landfield_details",
description: "Get detailed information about a specific property",
inputSchema: {
type: "object",
properties: {
landfield_id: {
type: "string",
description: "The landfield/property ID",
},
},
required: ["landfield_id"],
},
},
{
name: "get_landfield_resources",
description: "Get resource balances for a specific property",
inputSchema: {
type: "object",
properties: {
landfield_id: {
type: "string",
description: "The landfield/property ID",
},
},
required: ["landfield_id"],
},
},
// Marketplace
{
name: "get_marketplace",
description: "Browse Earth2 marketplace with advanced filtering",
inputSchema: {
type: "object",
properties: {
country: {
type: "string",
description: "Filter by country code (e.g., 'US', 'JP')",
},
landfieldTier: {
type: "number",
description: "Filter by tier (1-3)",
},
sorting: {
type: "string",
description: "Sort by: price, price_per_tile, date, etc.",
},
items: {
type: "number",
description: "Number of items to retrieve",
},
},
},
},
// Resources
{
name: "get_my_resources",
description: "Get all your resource balances (ether, essence, jewels, etc.)",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "get_resource_balance",
description: "Get balance for a specific resource",
inputSchema: {
type: "object",
properties: {
ticker: {
type: "string",
description: "Resource ticker (e.g., 'ETHER', 'ESS', 'SHARD')",
},
},
required: ["ticker"],
},
},
{
name: "get_claimable_ether",
description: "Get your claimable ether amount",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "get_resource_stakes",
description: "Get your staked resources",
inputSchema: {
type: "object",
properties: {},
},
},
// Jewels
{
name: "get_my_jewels",
description: "Get your jewels with advanced filtering",
inputSchema: {
type: "object",
properties: {
colorName: {
type: "string",
description: "Filter by color (e.g., 'BLUE', 'RUBY', 'EMERALD')",
},
tier: {
type: "number",
description: "Filter by tier (1-3)",
},
qualityLevel: {
type: "string",
description: "Filter by quality (COMMON, UNCOMMON, RARE, EPIC, LEGENDARY)",
},
stacked: {
type: "boolean",
description: "Show only stacked jewels",
},
},
},
},
{
name: "get_bazaar_jewels",
description: "Browse jewel offers on the bazaar",
inputSchema: {
type: "object",
properties: {
colorName: {
type: "string",
description: "Filter by color",
},
tier: {
type: "number",
description: "Filter by tier",
},
sortBy: {
type: "string",
description: "Sort field",
},
},
},
},
// Droids
{
name: "get_my_droids",
description: "Get your droids with filters",
inputSchema: {
type: "object",
properties: {
rarity: {
type: "string",
description: "Filter by rarity",
},
sortBy: {
type: "string",
description: "Sort field",
},
},
},
},
{
name: "get_bazaar_droids",
description: "Browse droid offers on the bazaar",
inputSchema: {
type: "object",
properties: {
droidRarity: {
type: "string",
description: "Filter by rarity (common, uncommon, rare, epic, legendary)",
},
sortBy: {
type: "string",
description: "Sort by: price, rarity, date",
},
sortDir: {
type: "string",
description: "Sort direction: asc or desc",
},
},
},
},
// Civilians
{
name: "get_my_civilians",
description: "Get your civilians with filters",
inputSchema: {
type: "object",
properties: {
faction: {
type: "string",
description: "Filter by faction",
},
level: {
type: "number",
description: "Filter by level",
},
},
},
},
{
name: "get_bazaar_civilians",
description: "Browse civilian offers on the bazaar",
inputSchema: {
type: "object",
properties: {
civilianFaction: {
type: "string",
description: "Filter by faction (e.g., 'space_force')",
},
civilianLevel: {
type: "number",
description: "Filter by level",
},
civilianOccupation: {
type: "string",
description: "Filter by occupation",
},
},
},
},
// Blueprints
{
name: "get_my_blueprints",
description: "Get your holobuilding blueprints",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "get_bazaar_blueprints",
description: "Browse blueprint offers on the bazaar",
inputSchema: {
type: "object",
properties: {
sort: {
type: "string",
description: "Sort by: trending, recent, price",
},
hideOwn: {
type: "boolean",
description: "Hide your own listings",
},
},
},
},
// Transactions & Trading
{
name: "get_balance_transactions",
description: "Get your balance change transactions",
inputSchema: {
type: "object",
properties: {
limit: {
type: "number",
description: "Number of transactions to retrieve",
},
},
},
},
{
name: "get_ongoing_trades",
description: "Get your active trades",
inputSchema: {
type: "object",
properties: {},
},
},
// Leaderboards
{
name: "get_players_leaderboard",
description: "Get top players leaderboard with flexible filtering",
inputSchema: {
type: "object",
properties: {
sort_by: {
type: "string",
description: "Sort by: credits_sum, tiles_count, holobuildings_count, hq_jewels_count",
},
country: {
type: "string",
description: "Filter by country code (e.g., 'US', 'BR', 'JP', 'UK')",
},
continent: {
type: "string",
description: "Filter by continent: africa, asia, europe, north_america, south_america, oceania, antarctica",
},
jewel_quality: {
type: "string",
description: "For hq_jewels sort (BRILLIANT, LUMINOUS, etc.)",
},
},
},
},
{
name: "get_player_countries_leaderboard",
description: "Get player leaderboard by country",
inputSchema: {
type: "object",
properties: {
sort_by: {
type: "string",
description: "Sort by: tiles_count, essence_claimed_in_24_hours, essence_claimed_in_7_days, max_property_purchased_cost",
},
},
},
},
{
name: "get_player_continents_leaderboard",
description: "Get player leaderboard by continent",
inputSchema: {
type: "object",
properties: {
sort_by: {
type: "string",
description: "Sort by: tiles_count, etc.",
},
},
},
},
{
name: "get_landfield_countries_leaderboard",
description: "Get landfield leaderboard by country",
inputSchema: {
type: "object",
properties: {
sort_by: {
type: "string",
description: "Sort by: tiles_count, essence_claimed_in_24_hours, essence_claimed_in_7_days, max_property_purchased_cost",
},
},
},
},
{
name: "get_landfield_continents_leaderboard",
description: "Get landfield leaderboard by continent",
inputSchema: {
type: "object",
properties: {
sort_by: {
type: "string",
description: "Sort by: tiles_count, etc.",
},
},
},
},
// Essence & Crypto
{
name: "get_essence_token_info",
description: "Get ESS token information",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "get_ess_usd_rate",
description: "Get current ESS/USD exchange rate",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "get_crypto_payouts",
description: "Get your crypto withdrawal/payout history",
inputSchema: {
type: "object",
properties: {},
},
},
// Activities & Quests
{
name: "get_quests",
description: "Get available quests",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "get_my_quests",
description: "Get your active and completed quests",
inputSchema: {
type: "object",
properties: {},
},
},
// Tiles & Pricing
{
name: "get_tile_prices",
description: "Get all tile prices by country",
inputSchema: {
type: "object",
properties: {},
},
},
],
};
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
// Initialize client if not already done
if (!this.earth2Client) {
this.initializeClient();
}
const { name, arguments: args } = request.params;
let result;
switch (name) {
// User Profile & Info
case "get_user_info":
result = await this.earth2Client.getUserInfo(args?.user_id);
break;
case "get_my_networth":
result = await this.earth2Client.getMyNetworth();
break;
case "search_users":
if (!args?.query) {
throw new McpError(ErrorCode.InvalidParams, "query is required");
}
result = await this.earth2Client.searchUsers(args.query);
break;
// Properties / Landfields
case "get_landfields":
result = await this.earth2Client.getLandfields(args || {});
break;
case "get_landfield_details":
if (!args?.landfield_id) {
throw new McpError(ErrorCode.InvalidParams, "landfield_id is required");
}
result = await this.earth2Client.getLandfieldDetails(args.landfield_id);
break;
case "get_landfield_resources":
if (!args?.landfield_id) {
throw new McpError(ErrorCode.InvalidParams, "landfield_id is required");
}
result = await this.earth2Client.getLandfieldBalanceResources(args.landfield_id);
break;
// Marketplace
case "get_marketplace":
result = await this.earth2Client.getMarketplace(args || {});
break;
// Resources
case "get_my_resources":
result = await this.earth2Client.getMyBalanceResources();
break;
case "get_resource_balance":
if (!args?.ticker) {
throw new McpError(ErrorCode.InvalidParams, "ticker is required");
}
result = await this.earth2Client.getMyResourceBalance(args.ticker);
break;
case "get_claimable_ether":
result = await this.earth2Client.getClaimableEtherAmount();
break;
case "get_resource_stakes":
result = await this.earth2Client.getMyResourceStakes();
break;
// Jewels
case "get_my_jewels":
result = await this.earth2Client.getMyJewels(args || {});
break;
case "get_bazaar_jewels":
result = await this.earth2Client.getBazaarJewelOffers(args || {});
break;
// Droids
case "get_my_droids":
result = await this.earth2Client.getDroids(args || {});
break;
case "get_bazaar_droids":
result = await this.earth2Client.getBazaarDroidOffers(args || {});
break;
// Civilians
case "get_my_civilians":
result = await this.earth2Client.getCivilians(args || {});
break;
case "get_bazaar_civilians":
result = await this.earth2Client.getBazaarCivilianOffers(args || {});
break;
// Blueprints
case "get_my_blueprints":
result = await this.earth2Client.getMyBlueprints();
break;
case "get_bazaar_blueprints":
result = await this.earth2Client.getBazaarBlueprintOffers(args || {});
break;
// Transactions & Trading
case "get_balance_transactions":
result = await this.earth2Client.getBalanceChangeTransactions(args || {});
break;
case "get_ongoing_trades":
result = await this.earth2Client.getMyOngoingTrades();
break;
// Leaderboards
case "get_players_leaderboard":
result = await this.earth2Client.getPlayersLeaderboard(args || {});
break;
case "get_player_countries_leaderboard":
result = await this.earth2Client.getPlayerCountriesLeaderboard(args || {});
break;
case "get_player_continents_leaderboard":
result = await this.earth2Client.getPlayerContinentsLeaderboard(args || {});
break;
case "get_landfield_countries_leaderboard":
result = await this.earth2Client.getLandfieldCountriesLeaderboard(args || {});
break;
case "get_landfield_continents_leaderboard":
result = await this.earth2Client.getLandfieldContinentsLeaderboard(args || {});
break;
// Essence & Crypto
case "get_essence_token_info":
result = await this.earth2Client.getEssenceTokenInfo();
break;
case "get_ess_usd_rate":
result = await this.earth2Client.getEssUsdRate();
break;
case "get_crypto_payouts":
result = await this.earth2Client.getMyCryptoPayouts();
break;
// Activities & Quests
case "get_quests":
result = await this.earth2Client.getQuests();
break;
case "get_my_quests":
result = await this.earth2Client.getMyQuests();
break;
// Tiles & Pricing
case "get_tile_prices":
result = await this.earth2Client.getTilePricesAll();
break;
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
if (error instanceof McpError) {
throw error;
}
throw new McpError(ErrorCode.InternalError, error.message);
}
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("Earth2 MCP server running on stdio");
}
}
const server = new Earth2MCPServer();
server.run().catch(console.error);