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 three _meta capabilities:
Optional contributor-side search helpers are not a new _meta contract in this rollout. If your venue needs the LLM-backed pattern described in Optional Contributor Search Helpers, keep that logic and any helper-private state inside the contributor. Standardized helper evidence now rides in contributor searchMetadata payloads and developer traces, not in ad hoc _meta.searchHelper fields.

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" },
      },
    },
    _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;

    const positions = portfolio.assetPositions || [];
    const accountValue = portfolio.marginSummary?.accountValue || 0;

    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"],
        },
        _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.
{
  marginSummary: {
    accountValue: string;
    totalMarginUsed: string;
    totalNtlPos: string;
    totalRawUsd: string;
  };
  assetPositions: Array<{
    position: {
      coin: string;
      szi: string;
      entryPx: string;
      positionValue: string;
      unrealizedPnl: string;
      leverage: {
        type: string;
        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.
{
  positions: Array<{
    market: string;
    outcome: string;
    size: number;
    avgPrice: number;
    currentPrice: number;
    pnl: number;
  }>;
  openOrders: Array<{
    market: string;
    side: "buy" | "sell";
    price: number;
    size: number;
  }>;
  balance: number;
}

"wallet" — Generic EVM Wallet

Basic wallet data for any EVM-compatible chain.
{
  address: string;
  chainId: number;
  balances: Array<{
    token: string;
    address: string;
    balance: string;
    decimals: number;
    usdValue?: number;
  }>;
  nativeBalance: string;
}

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");
    
    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];

    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);

Pricing & Execute Mode Eligibility

Most contributors start here: If you just want your tool available in the Context app (Query mode), you don’t need to configure anything in this section — set a listing response price when you register and you’re done. This section is for contributors who also want SDK developers to call their methods directly with per-call pricing (Execute mode).

How Query and Execute Work

Context runs one marketplace with two modes:
ModeWho is the librarian?BillingWhat you get
QueryContext runtime (used via the app or client.query.run() in the SDK)Pay-per-response (~$0.10)Managed librarian output: plain answer or structured evidence package assembled from up to 100 MCP calls
ExecuteYour app/agent (used via client.tools.execute() in the SDK)Per method call (~$0.001)Raw structured data with session spending limit visibility
SDK consumers can use both modes. Use client.query.run() when you want Context to be the librarian (answer, answer_with_evidence, or evidence_only, pay-per-response). Use client.tools.execute() when your agent is the librarian (raw data, per-call pricing, spending limits). See TypeScript SDK or Python SDK.
Execute pricing is typically ~1/100 of listing response price because a single Query response can invoke up to 100 method calls internally. When developers pay per call, they need proportionally lower prices.

Metadata Shape

Add these fields to each method definition under _meta:
FieldTypePurpose
surface"answer" | "execute" | "both"Which mode(s) this method is eligible for (API field name)
queryEligiblebooleanWhether the Query runtime can select this method for answer synthesis
latencyClass"instant" | "fast" | "slow" | "streaming"Expected response time class (Query excludes streaming by default)
pricing.executeUsdstringPer-call execute price in USD (required for Execute eligibility)
pricing.queryUsdstringReserved metadata for future per-method query billing (not used for billing in current rollout)

Execute Eligibility Rule

Hard gate: A method is Execute-eligible only when it has an explicit _meta.pricing.executeUsd value. There is no fallback to listing-level pricing for Execute calls.
  • Methods with _meta.pricing.executeUsd → visible in both Query and Execute (depending on surface field)
  • Methods without _meta.pricing.executeUsdQuery-only, invisible to SDK Execute discovery

Default Execute Price (Contributor Shortcut)

When you list your tool on the marketplace, you can set a single default execute price in the contribute form. This convenience input fans out to every method’s _meta.pricing.executeUsd at ingestion time. You don’t need to edit each method individually unless you want per-method overrides.
ApproachHow it works
Simple (recommended)Set one default execute price in the contribute form → applied to all methods
AdvancedSet _meta.pricing.executeUsd per method in your MCP server code → preserved on refresh
MixedDefault fills gaps; explicit method-level values take precedence

TypeScript Example

const TOOLS = [
  {
    name: "get_gas_prices",
    description: "Get current gas prices for EVM chains",
    _meta: {
      surface: "both",
      queryEligible: true,
      latencyClass: "instant",
      pricing: {
        executeUsd: "0.001",
      },
    },
    inputSchema: {
      type: "object",
      properties: {
        chainId: { type: "number", description: "EVM chain ID" },
      },
    },
    outputSchema: {
      type: "object",
      properties: {
        gasPrice: { type: "number" },
        unit: { type: "string" },
      },
      required: ["gasPrice", "unit"],
    },
  },
  {
    name: "stream_live_trades",
    description: "Stream real-time trade data (Execute only)",
    _meta: {
      surface: "execute",
      queryEligible: false,
      latencyClass: "streaming",
      pricing: {
        executeUsd: "0.0005",
      },
    },
    inputSchema: { type: "object", properties: {} },
    outputSchema: { type: "object", properties: {} },
  },
];

Python Example

TOOLS = [
    {
        "name": "get_gas_prices",
        "description": "Get current gas prices for EVM chains",
        "_meta": {
            "surface": "both",
            "queryEligible": True,
            "latencyClass": "instant",
            "pricing": {
                "executeUsd": "0.001",
            },
        },
        "inputSchema": {
            "type": "object",
            "properties": {
                "chainId": {"type": "number", "description": "EVM chain ID"},
            },
        },
        "outputSchema": {
            "type": "object",
            "properties": {
                "gasPrice": {"type": "number"},
                "unit": {"type": "string"},
            },
            "required": ["gasPrice", "unit"],
        },
    },
]

Choosing a Mode

Your tool is best for…Set surface toSet queryEligibleSet pricing.executeUsd
Curated intelligence (e.g., “smart money analysis”)"answer" or "both"trueOptional (set if you also want Execute revenue)
Normalized raw data (e.g., “cross-exchange prices”)"execute" or "both"false for execute-only, true for bothRequired
Both curated answers and raw data access"both"trueRequired
Mixed listings are supported. A single tool can have some methods for Query only (curated intelligence tools), some for Execute only (streaming or raw data), and some for both. The platform routes each method based on its individual metadata.
Visibility is bidirectional. The platform gates visibility in both directions:
  • No execute price → invisible in Execute. Methods without _meta.pricing.executeUsd will not appear in SDK Execute discovery (client.tools.execute()). They remain Query-only.
  • Execute-only → invisible in Query. Methods with surface: "execute" and queryEligible: false will not appear in the Context app or client.query.run(). The runtime will never select them for answer synthesis. They are SDK-only.
Design your surface and queryEligible settings intentionally — they control where your tool shows up.

Pricing Guidance

Listing response price (Query)Suggested execute price per methodRatio
$0.01$0.00011/100
$0.05$0.00051/100
$0.10$0.0011/100
$0.25$0.00251/100
The ~1/100 ratio is guidance, not a protocol-enforced rule. A Query response can make up to 100 method calls in one turn, so execute pricing per call should be proportionally lower.

Legacy Tools (No _meta)

Tools listed before this metadata rollout continue working in Query with unchanged economics. They receive default metadata at ingestion time:
FieldDefault
surface"both"
queryEligibletrue
latencyClass"slow"
pricing.executeUsdNot set → Query-only in Execute
To enable Execute visibility for a legacy tool, either set a default execute price in the Developer Tools page or add _meta.pricing.executeUsd to your MCP server code and click “Refresh Skills”.

Reference Implementation

The Coinglass contributor server demonstrates all _meta capabilities in production:
  • Per-method _meta.pricing.executeUsd with a default fallback and explicit opt-out for query-only methods
  • Per-method _meta.rateLimit hints built from upstream API tier constraints
  • Batch tool recommendations via recommendedBatchTools

Handshake Architecture

For write actions: signatures, transactions, OAuth

Build & List Your Tool

Complete guide to building MCP tools