Skip to main content
Every MCP tool definition supports a _meta field for platform-level configuration that sits alongside inputSchema and outputSchema. This guide covers the two main _meta capabilities:
  • Rate-Limit Hints — tell the planner and runtime how to pace calls to your API
  • Context Injection — declare what user-specific data the platform should inject into your tool’s arguments

Rate-Limit Hints

Use this section if your MCP server wraps APIs with strict quotas (hobby/free tiers, burst limits, or expensive fan-out endpoints).

Why This Exists

In the marketplace, each contributor has different upstream constraints. Without pacing hints, an agent can generate loops that are valid code but operationally unsafe for your API key tier. Publishing _meta.rateLimit (or _meta.rateLimitHints) gives Context two things:
  • Better planning (prefer batch/snapshot tools, avoid risky fan-out patterns)
  • Safer runtime pacing (respect cooldowns and concurrency intent)
This reduces avoidable 429/timeout loops and improves first-pass completion.

Metadata Shape

Add these fields to each tool definition under _meta.rateLimit:
FieldTypePurpose
maxRequestsPerMinutenumberThroughput budget for this tool
cooldownMsnumberMinimum delay between sequential calls
maxConcurrencynumberParallel call ceiling for this tool
supportsBulkbooleanWhether the tool already handles batch retrieval
recommendedBatchToolsstring[]Preferred alternatives to fan-out loops
notesstringHuman guidance for planner behavior

TypeScript Example

const TOOLS = [
  {
    name: "get_portfolio_snapshot",
    description: "Fetch portfolio snapshot for a wallet",
    _meta: {
      rateLimit: {
        maxRequestsPerMinute: 30,
        cooldownMs: 2000,
        maxConcurrency: 1,
        supportsBulk: true,
        recommendedBatchTools: ["get_portfolio_snapshot"],
        notes: "Hobby tier: prefer snapshot over per-asset fan-out loops.",
      },
    },
    inputSchema: { type: "object", properties: {} },
    outputSchema: { type: "object", properties: {} },
  },
];

Python Example

TOOLS = [
    {
        "name": "get_portfolio_snapshot",
        "description": "Fetch portfolio snapshot for a wallet",
        "_meta": {
            "rateLimit": {
                "maxRequestsPerMinute": 30,
                "cooldownMs": 2000,
                "maxConcurrency": 1,
                "supportsBulk": True,
                "recommendedBatchTools": ["get_portfolio_snapshot"],
                "notes": "Hobby tier: prefer snapshot over per-asset fan-out loops.",
            }
        },
        "inputSchema": {"type": "object", "properties": {}},
        "outputSchema": {"type": "object", "properties": {}},
    }
]

Contributor Guidance

  • Publish hints per tool, not only globally. Heavy fan-out endpoints and lightweight snapshot endpoints should have different guidance.
  • Keep maxConcurrency conservative for expensive or quota-sensitive endpoints.
  • Populate recommendedBatchTools whenever you offer a better aggregation endpoint.
  • Treat notes as planner guidance for real-world constraints (for example, “call alone”, “use shallow scan first”).
These hints are not a substitute for server-side protections. Keep your own upstream safeguards (timeouts, retries, and pacing) in the MCP server.

Rate-Limit Reference Implementations


Context Injection

The Context Injection pattern enables MCP tools to receive user-specific data without handling authentication or credentials:
  • Portfolio Data — Hyperliquid positions, balances, P&L
  • Prediction Markets — Polymarket positions and orders
  • Wallet Data — EVM addresses and token balances
Why “Context Injection”? The platform fetches user data client-side and injects it directly into your tool’s arguments. Your server never sees private keys or credentials — just structured data ready to analyze.

When to Use Context Injection

Use CaseContext TypeExample Query
Portfolio analysis"hyperliquid"”Analyze my Hyperliquid positions”
Position tracking"polymarket"”How are my Polymarket bets performing?”
Wallet insights"wallet"”What tokens do I hold on Base?”
User-specific vs. Public Data: Use Context Injection when your tool needs to answer “my” questions. For public data (e.g., “What’s the ETH price?”), you don’t need context injection — just fetch it directly.

Quick Start (TypeScript)

1. Declare Context Requirements

Add _meta.contextRequirements to your tool definition:
const TOOLS = [
  {
    name: "analyze_my_positions",
    description: "Analyze user's Hyperliquid perpetual positions",
    inputSchema: {
      type: "object",
      properties: {
        portfolio: {
          type: "object",
          description: "User's Hyperliquid portfolio (injected by platform)",
        },
      },
      required: ["portfolio"],
    },
    outputSchema: {
      type: "object",
      properties: {
        totalPnl: { type: "number" },
        topPosition: { type: "string" },
        riskScore: { type: "number" },
      },
    },
    // 👇 This tells the platform to inject Hyperliquid data
    _meta: {
      contextRequirements: ["hyperliquid"],
    },
  },
];

2. Handle the Injected Data

The platform injects the data as the portfolio argument:
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "analyze_my_positions") {
    const { portfolio } = request.params.arguments;

    // portfolio contains structured Hyperliquid data
    const positions = portfolio.assetPositions || [];
    const accountValue = portfolio.marginSummary?.accountValue || 0;

    // Your analysis logic
    const totalPnl = positions.reduce(
      (sum, p) => sum + (p.position?.unrealizedPnl || 0),
      0
    );

    const topPosition = positions.sort(
      (a, b) => Math.abs(b.position?.szi || 0) - Math.abs(a.position?.szi || 0)
    )[0];

    return {
      content: [{ type: "text", text: `Total PnL: \$${totalPnl.toFixed(2)}` }],
      structuredContent: {
        totalPnl,
        topPosition: topPosition?.position?.coin || "None",
        riskScore: calculateRiskScore(positions),
      },
    };
  }
});

Quick Start (Python)

1. Declare Context Requirements

from mcp.server import Server
from mcp.types import Tool

tools = [
    Tool(
        name="analyze_my_positions",
        description="Analyze user's Hyperliquid perpetual positions",
        inputSchema={
            "type": "object",
            "properties": {
                "portfolio": {
                    "type": "object",
                    "description": "User's Hyperliquid portfolio (injected by platform)",
                },
            },
            "required": ["portfolio"],
        },
        # 👇 This tells the platform to inject Hyperliquid data
        _meta={
            "contextRequirements": ["hyperliquid"],
        },
    ),
]

2. Handle the Injected Data

@server.call_tool()
async def handle_tool(name: str, arguments: dict):
    if name == "analyze_my_positions":
        portfolio = arguments.get("portfolio", {})

        positions = portfolio.get("assetPositions", [])
        account_value = portfolio.get("marginSummary", {}).get("accountValue", 0)

        total_pnl = sum(
            p.get("position", {}).get("unrealizedPnl", 0)
            for p in positions
        )

        return {
            "content": [{"type": "text", "text": f"Total PnL: ${total_pnl:.2f}"}],
            "structuredContent": {
                "totalPnl": total_pnl,
                "accountValue": float(account_value),
                "positionCount": len(positions),
            },
        }

How It Works

┌─────────────────────────────────────────────────────────────────────┐
│  1. User: "Analyze my Hyperliquid positions"                        │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│  2. Context App checks tool's _meta.contextRequirements             │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│  3. App fetches user's Hyperliquid data (client-side, public API)   │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│  4. App injects data as `portfolio` argument to your tool           │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│  5. Your tool receives structured data, returns analysis            │
└─────────────────────────────────────────────────────────────────────┘

Supported Context Types

"hyperliquid" — Perpetuals & Spot

Hyperliquid portfolio data for perpetual and spot positions.
// Injected portfolio structure
{
  marginSummary: {
    accountValue: string;      // Total account value in USD
    totalMarginUsed: string;   // Margin currently in use
    totalNtlPos: string;       // Total notional position size
    totalRawUsd: string;       // Raw USD balance
  };
  assetPositions: Array<{
    position: {
      coin: string;            // e.g., "ETH", "BTC"
      szi: string;             // Position size (signed)
      entryPx: string;         // Entry price
      positionValue: string;   // Current position value
      unrealizedPnl: string;   // Unrealized P&L
      leverage: {
        type: string;          // "cross" or "isolated"
        value: number;
      };
    };
  }>;
  crossMarginSummary: { ... };
}
Data Source: Fetched from the public Hyperliquid Info API using the user’s linked wallet address.

"polymarket" — Prediction Markets

Polymarket positions, orders, and market data.
// Injected portfolio structure
{
  positions: Array<{
    market: string;            // Market question
    outcome: string;           // "Yes" or "No"
    size: number;              // Position size
    avgPrice: number;          // Average entry price
    currentPrice: number;      // Current market price
    pnl: number;               // Profit/loss
  }>;
  openOrders: Array<{
    market: string;
    side: "buy" | "sell";
    price: number;
    size: number;
  }>;
  balance: number;             // Available USDC balance
}

"wallet" — Generic EVM Wallet

Basic wallet data for any EVM-compatible chain.
// Injected portfolio structure
{
  address: string;             // User's wallet address (0x...)
  chainId: number;             // Current chain ID
  balances: Array<{
    token: string;             // Token symbol
    address: string;           // Token contract address
    balance: string;           // Balance in wei
    decimals: number;
    usdValue?: number;         // USD value if available
  }>;
  nativeBalance: string;       // ETH/native token balance in wei
}

Multiple Context Types

Your tool can request multiple context types:
{
  name: "cross_platform_analysis",
  _meta: {
    contextRequirements: ["hyperliquid", "wallet"]
  },
  inputSchema: {
    type: "object",
    properties: {
      hyperliquidPortfolio: { type: "object" },
      walletPortfolio: { type: "object" },
    },
  },
}
Naming Convention: When requesting multiple contexts, the platform injects each with a descriptive key (e.g., hyperliquidPortfolio, walletPortfolio). Check your tool’s argument names match what you declare in inputSchema.

Context Injection Best Practices

The user might not have linked the required account, or they might have empty positions:
if (!portfolio || !portfolio.assetPositions) {
  return {
    content: [{ type: "text", text: "No Hyperliquid data available. Please link your wallet in Settings." }],
    structuredContent: { error: "NO_DATA", message: "Portfolio not linked" },
  };
}
A linked wallet with no positions is valid — don’t treat it as an error:
if (positions.length === 0) {
  return {
    content: [{ type: "text", text: "No open positions found." }],
    structuredContent: { positions: [], summary: "No active positions" },
  };
}
Make it clear in your tool description that portfolio data is required:
description: "Analyze user's Hyperliquid positions. Requires linked Hyperliquid wallet."
Your analysis results should be machine-readable:
outputSchema: {
  type: "object",
  properties: {
    totalPnl: { type: "number", description: "Total unrealized P&L in USD" },
    riskLevel: { type: "string", enum: ["low", "medium", "high"] },
    recommendations: { type: "array", items: { type: "string" } },
  },
}

Security Model

Zero Credential Exposure: Your MCP server never sees private keys, API secrets, or wallet signatures. The platform fetches public data client-side and passes only the structured result to your tool.
Security PropertyHow It’s Achieved
No private keys exposedPlatform fetches data client-side
No API keys neededUses public APIs (Hyperliquid Info, etc.)
User controls data sharingUser explicitly links accounts in Settings
Read-only by defaultContext injection is read-only; use Handshakes for write actions

Future Context Types

Beyond Crypto: The Context Injection system is designed to support any type of user data. We’re starting with high-value crypto data, but the architecture supports identity, preferences, and external service data.
Planned context types (not yet available):
Context TypeDescription
"dydx"dYdX perpetual positions
"aave"Aave lending positions
"uniswap"Uniswap LP positions
"ens"ENS names and records
Want a specific context type? Open an issue describing your use case and the data you need.

Complete Example: Portfolio Analyzer

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { createContextMiddleware } from "@ctxprotocol/sdk";
import express from "express";

const app = express();
app.use(express.json());
app.use("/mcp", createContextMiddleware());

const TOOLS = [
  {
    name: "analyze_portfolio_risk",
    description: "Analyze risk metrics for user's Hyperliquid portfolio",
    inputSchema: {
      type: "object",
      properties: {
        portfolio: { type: "object" },
        riskTolerance: {
          type: "string",
          enum: ["conservative", "moderate", "aggressive"],
          default: "moderate",
        },
      },
      required: ["portfolio"],
    },
    outputSchema: {
      type: "object",
      properties: {
        riskScore: { type: "number", minimum: 0, maximum: 100 },
        leverageRatio: { type: "number" },
        largestPosition: { type: "string" },
        recommendations: { type: "array", items: { type: "string" } },
      },
    },
    _meta: {
      contextRequirements: ["hyperliquid"],
    },
  },
];

const server = new Server(
  { name: "portfolio-analyzer", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "analyze_portfolio_risk") {
    const { portfolio, riskTolerance = "moderate" } = request.params.arguments;

    if (!portfolio?.assetPositions) {
      return {
        content: [{ type: "text", text: "Please link your Hyperliquid wallet in Settings." }],
        structuredContent: { error: "NO_PORTFOLIO" },
      };
    }

    const positions = portfolio.assetPositions;
    const accountValue = parseFloat(portfolio.marginSummary?.accountValue || "0");
    
    // Calculate metrics
    const totalNotional = positions.reduce(
      (sum, p) => sum + Math.abs(parseFloat(p.position?.positionValue || "0")),
      0
    );
    const leverageRatio = accountValue > 0 ? totalNotional / accountValue : 0;
    
    const largestPosition = positions.sort(
      (a, b) => 
        Math.abs(parseFloat(b.position?.positionValue || "0")) -
        Math.abs(parseFloat(a.position?.positionValue || "0"))
    )[0];

    // Risk scoring
    let riskScore = 0;
    if (leverageRatio > 5) riskScore += 40;
    else if (leverageRatio > 2) riskScore += 20;
    
    if (positions.length > 10) riskScore += 20;
    if (positions.some(p => parseFloat(p.position?.leverage?.value || 0) > 10)) riskScore += 30;

    const recommendations = [];
    if (leverageRatio > 3) recommendations.push("Consider reducing leverage");
    if (positions.length > 8) recommendations.push("Portfolio may be over-diversified");

    return {
      content: [{
        type: "text",
        text: `Risk Score: ${riskScore}/100. Leverage: ${leverageRatio.toFixed(2)}x`,
      }],
      structuredContent: {
        riskScore,
        leverageRatio,
        largestPosition: largestPosition?.position?.coin || "None",
        recommendations,
      },
    };
  }
});

app.listen(3000);