Skip to main content
Glama
RJW34

Weather Edge MCP Server

get_weather_signals

Retrieve calibrated edge signals for Kalshi weather prediction markets by providing a city name. Uses NWS and GFS ensembles to identify mispriced temperature opportunities.

Instructions

Get calibrated edge signals for one city's Kalshi weather markets.

Args: city: One of nyc, chicago, denver, miami, la.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
cityYes

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
resultYes

Implementation Reference

  • The MCP tool handler for get_weather_signals. Decorated with @mcp.tool(), it takes a city name, looks up the city config, runs compute_signals asynchronously, and formats the result via format_weather_signals.
    @mcp.tool()
    def get_weather_signals(city: str) -> str:
        """Get calibrated edge signals for one city's Kalshi weather markets.
    
        Args:
            city: One of nyc, chicago, denver, miami, la.
        """
        cfg = get_city(city)
        return format_weather_signals(_run(compute_signals(cfg.key)))
  • FastMCP server registration with instructions mentioning get_weather_signals in the usage description.
    mcp = FastMCP(
        name="weather-edge",
        instructions=(
            "Weather Edge MCP Server for calibrated Kalshi weather-market intelligence. "
            "Use list_cities for supported markets, get_weather_signals for one city, "
            "get_all_signals for a full scan, get_forecast for raw forecast context, and "
            "get_station_observation for live settlement-station readings."
        ),
    )
  • format_weather_signals - formats the computed signal data dict into a human-readable string for the get_weather_signals tool response.
    def format_weather_signals(data: dict[str, Any]) -> str:
        if data.get("error"):
            return f"Error: {data['error']}"
        lines = [f"# Weather Edge — {data['city_label']}", f"Station: {data['station']}", ""]
        forecast = data["forecast"]
        lines.append(f"Forecast: {forecast['high_f']}°F — {forecast['forecast']}")
        lines.append("")
        for signal in data.get("signals", [])[:10]:
            if not signal.get("verdict"):
                continue
            lines.append(
                f"- [{signal['verdict']}] {signal['bucket']} | NWS {signal['nws_prob']}% vs market {signal['market_price']}% | edge {signal['edge']:+.1f} pts | EV {signal['net_ev_cents']:+.1f}c"
            )
        if len(lines) <= 4:
            lines.append("No positive-EV signals found.")
        return "\n".join(lines)
  • compute_signals - the core async function that fetches NWS forecast and Kalshi markets, computes probabilities and edges, and returns a structured signal dict ready for formatting.
    async def compute_signals(city_key: str) -> dict[str, Any]:
        cached = get_cached(f"signals_{city_key}")
        if cached:
            return cached
    
        cfg = CITIES[city_key]
        forecast = await fetch_nws_forecast(city_key)
        if not forecast:
            return {"city": city_key, "error": "NWS unavailable", "signals": []}
    
        markets = await fetch_kalshi_markets(city_key)
        signals: list[dict[str, Any]] = []
    
        for market in markets:
            subtitle = market.get("subtitle", market.get("yes_sub_title", ""))
            yes_bid = float(market.get("yes_bid_dollars", 0) or 0)
            yes_ask = float(market.get("yes_ask_dollars", 0) or 0)
            volume = float(market.get("volume_fp", market.get("volume", 0)) or 0)
    
            is_over = "or above" in subtitle.lower() or "greater" in market.get("strike_type", "")
            is_under = "or below" in subtitle.lower()
            low_f = high_f = None
            nums = re.findall(r"(\d+)", subtitle)
            if is_over and nums:
                low_f = int(nums[0])
            elif is_under and nums:
                high_f = int(nums[0])
            elif len(nums) >= 2:
                low_f, high_f = int(nums[0]), int(nums[1])
    
            nws_prob = compute_probability(
                forecast["high_f"],
                low_f,
                high_f,
                is_over,
                is_under,
                sigma=cfg.sigma,
                forecast_bias=cfg.forecast_bias,
            )
            mid_price = (yes_bid + yes_ask) / 2 if yes_ask > 0 else yes_bid
            if mid_price <= 0:
                continue
    
            edge = nws_prob - mid_price
            fee = 0.07 * mid_price * (1 - mid_price)
            net_ev = nws_prob * (1 - yes_ask) - (1 - nws_prob) * yes_ask - fee if yes_ask > 0 else 0
            verdict = "STRONG" if net_ev > 0.05 else ("GOOD" if net_ev > 0.02 else ("MARGINAL" if net_ev > 0 else ""))
    
            signals.append(
                {
                    "ticker": market.get("ticker", ""),
                    "bucket": subtitle,
                    "date": forecast["date"],
                    "nws_high": forecast["high_f"],
                    "nws_prob": round(nws_prob * 100, 1),
                    "market_price": round(mid_price * 100, 1),
                    "edge": round(edge * 100, 1),
                    "net_ev_cents": round(net_ev * 100, 1),
                    "volume": int(volume),
                    "yes_bid": yes_bid,
                    "yes_ask": yes_ask,
                    "verdict": verdict,
                }
            )
    
        signals.sort(key=lambda signal: signal["net_ev_cents"], reverse=True)
        result = {
            "city": city_key,
            "city_label": cfg.label,
            "station": cfg.station,
            "forecast": forecast,
            "signals": signals,
            "generated_at": datetime.now(timezone.utc).isoformat(),
        }
        set_cached(f"signals_{city_key}", result)
        return result
  • get_city - looks up a CityConfig by city key, used by the handler to resolve the city string to a configuration.
    def get_city(city: str) -> CityConfig:
        key = city.lower().strip()
        if key not in CITIES:
            raise ValueError(f"Unknown city '{city}'. Valid: {', '.join(CITIES.keys())}")
        return CITIES[key]
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations exist, so the description carries the burden. It implies a read operation ('Get') and lists city options but does not explicitly state side effects, permissions, or rate limits. However, the simple retrieval nature and presence of output schema mitigate missing details.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is extremely concise (two sentences), front-loads the purpose, and includes the parameter specification without redundancy. Every word adds value.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With a single parameter and an output schema, the description covers the essential purpose and allowed values. It does not explain return structure (handled by output schema) or any prerequisites, but is sufficient for basic usage.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has 0% description coverage and no enum, but the description adds the allowed values 'nyc, chicago, denver, miami, la' for the city parameter, fully compensating for the schema gap.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description specifies the verb 'Get' and the resource 'calibrated edge signals for one city's Kalshi weather markets', clearly distinguishing it from siblings like get_all_signals (broader) and get_forecast (different resource).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly lists the allowed city values and states 'one city', but does not explicitly guide when to use this tool vs alternatives like get_all_signals for multiple cities.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/RJW34/weather-edge-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server