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.
Add these fields to each tool definition under _meta.rateLimit:
Field Type Purpose maxRequestsPerMinutenumber Throughput budget for this tool cooldownMsnumber Minimum delay between sequential calls maxConcurrencynumber Parallel call ceiling for this tool supportsBulkboolean Whether the tool already handles batch retrieval recommendedBatchToolsstring[] Preferred alternatives to fan-out loops notesstring Human 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 Case Context Type Example 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
Validate the injected data
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" },
};
}
Handle empty portfolios gracefully
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" },
};
}
Document the portfolio argument
Make it clear in your tool description that portfolio data is required: description : "Analyze user's Hyperliquid positions. Requires linked Hyperliquid wallet."
Use outputSchema for structured responses
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 Property How It’s Achieved No private keys exposed Platform fetches data client-side No API keys needed Uses public APIs (Hyperliquid Info, etc.) User controls data sharing User explicitly links accounts in Settings Read-only by default Context 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 Type Description "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 );