MCP server & AI agents
The Off-Nadir Delta MCP server lets an AI agent pull live, geolocated world event intelligence — query events and what changed about them, search satellite scenes and plan a collection, read the Daily World Brief, keep a Watchlist, and assess an event or ask an analyst. It serves the tools meant for a conversation; the REST API and the Python SDK carry everything, with every field — for agents that run code and ChatGPT custom GPTs see the end of this page. The server is one stateless Streamable HTTP (JSON-RPC 2.0) endpoint at /api/v1/mcp and works with any MCP-capable client.
Authentication
Two ways to authenticate, in order of preference:
- OAuth 2.1 (recommended for interactive clients such as claude.ai remote connectors). The endpoint is an OAuth 2.1 resource server with discovery, dynamic client registration, and PKCE — compatible clients complete the flow automatically; you just log in and approve.
- Static API key (for CLIs, scripts, and servers). Pass your
ond_…key as a bearer token. Create one in your Developer API settings.
Available on every plan, including Free. What a tool may return follows your plan — a refusal carries denial naming the plan that opens it, and a filter or window your plan does not include is reported in meta instead of being applied. Each metered tool spends from the same monthly token allowance as the app (see each tool's cost below); get_world_brief and get_usage are free. When your balance runs out, calls return a clear error until it resets or you top up.
Client setup
Connecting from the Claude or ChatGPT apps? Those use the one-click OAuth flow (no API key to paste) — start with the two sections below. Wiring it into a CLI, editor, or script? Skip to Claude Code and use a static ond_… key. Everywhere, the endpoint is the same:
https://offnadir-delta.com/api/v1/mcpClaude.ai & Claude Desktop (remote connector)
No API key needed — authentication is handled by OAuth. In claude.ai or Claude Desktop, open Settings → Connectors → Add custom connector, paste the endpoint URL above, then Connect and approve on the Off-Nadir Delta login screen. Claude registers itself automatically (dynamic client registration + PKCE) and completes the flow — you just log in and grant access. How many custom connectors you can add depends on your Claude plan (Anthropic documents the current limits); the connection then spends from your Off-Nadir Delta token balance, and you can revoke it anytime from Developer API settings.
ChatGPT (Developer Mode connector)
Also key-free via OAuth. In ChatGPT, enable Settings → Connectors → Advanced → Developer mode, then Add custom connector, paste the endpoint URL above, and complete the login on the Off-Nadir Delta approval screen. Developer Mode is not available on every ChatGPT plan — check whether yours exposes it. As with Claude, the connection is metered on your token balance and revocable from your Developer API settings.
Claude Code
claude mcp add --transport http off-nadir-delta \
https://offnadir-delta.com/api/v1/mcp \
--header "Authorization: Bearer ond_..."Claude Desktop & generic clients
Add to your mcpServers configuration:
{
"mcpServers": {
"off-nadir-delta": {
"url": "https://offnadir-delta.com/api/v1/mcp",
"headers": {
"Authorization": "Bearer ond_..."
}
}
}
}Cursor
One-click install, or add the same block to ~/.cursor/mcp.json manually. You complete OAuth (or add your ond_… key) on first use.
VS Code
One-click install, or add to .vscode/mcp.json manually (use the servers key with "type": "http" and the endpoint URL).
Python (programmatic)
from offnadir_delta import McpClient
with McpClient(api_key="ond_...") as mcp:
mcp.initialize()
print([t["name"] for t in mcp.list_tools()])
result = mcp.call_tool("query_signals", {"bbox": [22, 44, 40, 53], "recency": "24h"})First call (free, no tokens)
Verify your key without spending anything: get_usage and get_world_brief cost zero tokens. This JSON-RPC call returns your remaining balance and plan capabilities:
curl -s https://offnadir-delta.com/api/v1/mcp \
-H "Authorization: Bearer ond_..." \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_usage","arguments":{}}}'Then try get_world_brief (also free) for today's AI-synthesized world digest, and call get_usage again before any metered tool to confirm you have enough balance.
Tools
Discover
What is happening, what changed, and what today looks like.
query_signals3 tokGeolocated world events from global news media, AI-enriched with severity/GEOINT scores and collection recommendations.
get_world_brieffreeAI world brief: daily (prior UTC day, all plans) or weekly/monthly by plan.
query_developments3 tokWhat actually CHANGED about the events in an area, not which articles are new.
get_event_threadfreeOne event end to end: state plus every change in order — the "new event or update" distinction a feed cannot make.
search_entities1 tokFind a named place (airport, base, plant, port, dam, strait…) in any language or by IATA/ICAO code.
get_entity1 tokWhat has happened at one place: the registry record plus its most recent linked events, each with HOW it was linked.
get_related_events1 tokThe relations recorded for one event: others at the same registry facility, naming the same place, or stored as the same campaign, each saying what is shared; plus reports merged into it.
Plan
Whether a satellite can resolve it, which one, and when it next passes.
search_imagery2 tokSearch the imagery catalog over an area and window.
plan_event_imagery4 tokThe deterministic imagery plan for ONE event: checks BOTH sensors exactly once — sentinel-1-grd (SAR, the only look that survives cloud and night) and sentinel-2-l2a — against the event footprint and a pre/post window.
rank_imaging_priority1 tokWHERE, and with what class of satellite, observation is most worthwhile now: composite IMPORTANCE crossed with the SPEC CLASS the required resolution demands — coarse, hr (free Sentinel-class) or vhr.
predict_satellite_passes2 tokWHEN a place can next be imaged and by WHAT: 13 free-systematic and commercial-taskable families.
Analyze
Turn reporting into a cited assessment you can audit afterwards.
assess_signal5/15 tokAI remote-sensing deep-dive for one signal: what to observe, sensors, a collection window.
ask_analyst5–123 tokAsk the Delta Analyst an OSINT/GEOINT question; returns a structured brief.
get_analyst_jobfreeStatus and result of an ask_analyst run.
Watch
Stand up continuous coverage and be told only when the answer changes. Creating and listing cost nothing — you are metered only when a check actually fires, or a new acquisition is actually measured.
list_watchesfreeThe Watchlist as one list, each entry with a state bucket.
list_notificationsfreeWhat changed across the Watchlist, newest first: one row per change a watch reported, with read state.
get_watchfreeOne watch end to end: target, state, latest change, measurements with recent series, standing-order questions, and for an event watch its stage, developments, imagery check and thread, plus this account's notes.
create_watchfreeAdd a target to the Watchlist.
update_watchfreePause, resume or close a watch.
delete_watchfreeDelete a watch and its underlying resources — bound monitored areas with their history, and standing orders.
add_notefreeAdd a note to a watch, start a thread, or reply.
Account
Pre-flight your token balance before a metered call.
get_usagefreeThe calling key's remaining token balance and plan capabilities.
Every result carries a one-line natural-language summary you can relay as-is, and successful results include structuredContent (matching each tool's outputSchema) for clients that consume parsed output.
MCP or the API?
A chat client loads every tool into its context and keeps every result in the conversation, and some clients cut a long result. So the MCP server is kept small: it serves the tools you use in a conversation, and its list results are digests on small pages — query_signals up to 50 rows (default 20), search_imagery up to 25 scenes without per-band asset links or the footprint polygon, and query_developments up to 50. Each list result carries full_records: the same query as a REST URL and a Python SDK call, which return every field and larger pages. An agent that can run code (Claude Code, Codex, Cursor) can follow it directly.
These operations are on the REST API and the SDK only. Calling one over MCP returns code: "api_only" with the operation to use:
| Former MCP tool | REST | Python SDK |
|---|---|---|
| survey_observable_events | GET /api/v1/collection/observability | client.collection.observability(...) |
| create_standing_order | POST /api/v1/standing-orders | client.standing_orders.create(...) |
| list_standing_orders | GET /api/v1/standing-orders | client.standing_orders.list() |
| list_layer_sets | GET /api/v1/layer-sets | client.workspace.list_layer_sets() |
| get_layer_set | GET /api/v1/layer-sets/{layerSetId} | client.workspace.get_layer_set(layer_set_id) |
| list_uploaded_layers | GET /api/v1/uploads | client.workspace.list_uploads() |
| list_monitored_areas | GET /api/v1/monitoring | client.monitoring.list() |
| get_monitored_area | GET /api/v1/monitoring/{areaId} | client.monitoring.get(area_id) |
| create_monitored_area | POST /api/v1/monitoring | client.monitoring.create(...) |
| delete_note | DELETE /api/v1/watches/{watchId}/notes/{noteId} | client.watches.delete_note(watch_id, note_id) |
| lookup_elevation | GET /api/v1/elevation | client.elevation.get(...) |
| analyze_terrain | GET /api/v1/terrain | client.terrain.sar_geometry(...) / client.terrain.profile(...) |
| measure_index_series | POST /api/v1/index-series | client.index_series.measure(...) |
| detect_ships | POST /api/v1/ships | client.ships.detect(...) |
Try it
Once the server is connected, paste one of these to see the tools chain together. Check your balance first with get_usage (free).
Daily situation briefing (starts free)
Using the Off-Nadir Delta connector, read the latest World Brief (get_world_brief), then list the most severe world-event signals reported in the last 24 hours (query_signals), each with its place, category and sources. Group them by category and flag anything escalating.What changed in an area
With Off-Nadir Delta, show what changed about the events in the Black Sea region (bbox 22, 44, 40, 53) today (query_developments): new events told apart from updates, and why each matters. Open the most important one end to end (get_event_thread).Signal → satellite plan
Find a severe security signal in Ukraine from the last 24 hours with Off-Nadir Delta (query_signals), plan the imagery for it (plan_event_imagery), and tell me when a satellite next passes over it (predict_satellite_passes).Agents that run code: use the SDK
An agent that can run code (Claude Code, Codex, Cursor) gets more from the Python SDK than from MCP: every field, every page, and every operation. Set the key in its environment and give it the instructions below — for example in AGENTS.md or CLAUDE.md at the root of your project.
pip install offnadir-delta
export OFFNADIR_DELTA_API_KEY=ond_...## Off-Nadir Delta (geolocated world events, satellite imagery planning)
- Use the Python SDK: `pip install offnadir-delta`. The API key is in the
OFFNADIR_DELTA_API_KEY environment variable; never print it or write it to a file.
- `from offnadir_delta import Client`; `client = Client()` reads the key.
- Before a metered call, check the balance with `client.usage()` (free).
- Events: `client.signals.list(bbox=[minLon, minLat, maxLon, maxLat], recency="24h")`;
every page: `client.signals.iterate(...)`. One event in full:
`client.developments.thread(event_id)`.
- A response states what the plan did not apply or withheld: read
`meta.filter_clamp`, `meta.window_clamp` and each row's `plan_lock` before
concluding that something is absent.
- Reference: https://offnadir-delta.com/docs/api (every endpoint),
https://offnadir-delta.com/docs#python-sdk, runnable programs in the python/ folder of
https://github.com/Off-Nadir-Lab/offnadir-delta-examples.ChatGPT custom GPT: GPT Actions
GPT Actions take an OpenAPI document with short descriptions and responses under 100,000 characters. This document carries the conversational operations of the REST API, cut to those limits, with smaller pages (querySignals up to 15 rows, searchImagery up to 8 scenes):
https://offnadir-delta.com/api/v1/openapi-gpt-actions.json- In the GPT editor, open Configure and add an action.
- Import the schema from the URL above (or paste the JSON it returns).
- Set authentication to an API key sent as a Bearer token, and enter your
ond_…key. - For the privacy policy, use https://offnadir-delta.com/legal/privacy-policy.
- Add instructions such as the ones below.
You answer questions about world events and satellite imagery with the
Off-Nadir Delta actions. Call getUsage before a metered action. Keep pages small
(querySignals limit 15 or less). A null value next to plan_lock means
the plan does not show it, not that it is unknown. Cite the event id and the
sources the response gives; do not add facts it does not contain.The key belongs to the GPT, not to each person using it: every call made through the GPT spends that key's tokens. Keep the GPT to yourself unless you intend that.
Tool argument schemas
Every tool's arguments, types, constraints, and token cost — rendered from the live server definition. Required arguments are marked; everything else is optional.
query_signalsMetered · 3 tokGeolocated world events from global news media, AI-enriched with severity/GEOINT scores and collection recommendations.
Geolocated world events (geopolitical, security, disaster, infrastructure) distilled from global news media and AI-enriched with severity/GEOINT scores and collection recommendations. Filter by bbox, date window and category.
| Parameter | Type | Req. | Constraints | Description |
|---|---|---|---|---|
| bbox | number[] | no | 4 items | [minLon, minLat, maxLon, maxLat] WGS84. Omit for worldwide. |
| startDate | string | no | — | Date range start, YYYY-MM-DD (UTC). With endDate. Records start 2026-09-23 (earlier: unrecorded, not quiet). Plan-bounded: meta.window_clamp. |
| endDate | string | no | — | Date range end, YYYY-MM-DD (UTC), inclusive. |
| recency | string | no | 15m | 1h | 24h | Last reported within. Default 24h when no date range. |
| categories | string[] | no | kinetic | armed_conflict | maritime | natural_disaster | infrastructure | aviation | humanitarian | protest | diplomacy | other | thermal_anomaly | transit_anomaly | nightlight_anomaly | so2_anomaly | quake_exposure | cyclone_exposure | Restrict to these categories. Measured or official-record events (not reporting; never a cause): thermal_anomaly=heat sources; transit_anomaly=ship transits off their recent level; nightlight_anomaly=city night lights under half their usual level; so2_anomaly=SO2 over a volcano; quake_exposure=earthquake near registered facilities; cyclone_exposure=places inside forecast cyclone wind radii. |
| q | string | no | — | Text search: all words; "phrase"; -exclude. |
| stages | string[] | no | reported | localized | pinpointed | reported=country/province, localized=town, pinpointed=block/facility. |
| minSeverityBand | integer | no | 4 | 6 | 9 | severity_band >= this. |
| minPublishers | integer | no | 2 | 3 | 5 | At least this many publishers. |
| markets | string[] | no | oil | natural_gas | grain | shipping | defense | metals | semiconductors | fx | equities | Exposed markets (physical/supply channel). |
| placement | string | no | all | map | list | map=a point; list=country/province only. |
| linkedTo | string | no | — | Shares a connector: facility:<entity_id>, place:<place_key>, actor:<name>. Plan-gated. |
| watchId | string | no | — | Events linked to this watch (list_watches id). |
| sort | string | no | latest | oldest | geoint | Default latest. geoint (plan-gated): observable, then geoint_score. |
| updatedSince | string | no | — | Refolded at/after (ISO). |
| limit | integer | no | 1…50 | Page size (default 20). Rows are digests; full rows: full_records. |
| cursor | string | no | — | From meta.next_cursor. |
Result (structuredContent)
| Field | Type | Always | Description |
|---|---|---|---|
| meta | object | yes | Query echo, token charge/balance (meta.tokens), and pagination where applicable. |
| summary | string | no | One-line natural-language summary of the result, ready to relay to a user. |
| signals | object[] | yes | |
| full_records | object | no | The same query on the REST API and the Python SDK, which return every field and larger pages. |
get_world_briefFreeAI world brief: daily (prior UTC day, all plans) or weekly/monthly by plan. freshness.is_stale: no newer day, so relay as stale.
The World Brief — an AI digest of worldwide event signals (headline, executive summary, top developments, per-theme roll-up, ranked signals). period=daily (default) covers the previous UTC day and is open on every plan; weekly and monthly are opened by the key owner's plan. If freshness.is_stale, a newer day is not yet available, so relay it as possibly out of date.
| Parameter | Type | Req. | Constraints | Description |
|---|---|---|---|---|
| period | string | no | daily | weekly | monthly | |
| date | string | no | — | YYYY-MM-DD UTC (daily: within the plan window); weekly/monthly: last day. Default latest |
Result (structuredContent)
| Field | Type | Always | Description |
|---|---|---|---|
| brief | object | yes | |
| freshness | object | no | Freshness of the returned brief: brief_date, generated_at, age_hours, freshness (operational|delayed|degraded), is_stale, note. |
get_usageFreeThe calling key's remaining token balance and plan capabilities. Pre-flight a metered call with it.
The calling key's remaining token balance and plan capabilities. Pre-flight a metered call with it.
No arguments.
Result (structuredContent)
| Field | Type | Always | Description |
|---|---|---|---|
| tokens | object | yes | |
| plan | object | yes |
search_imageryMetered · 2 tokSearch the imagery catalog over an area and window. Metadata only, no pixels. eventDate tags each scene pre/post — **a same-day scene is same_day_unknown, never post** — and eventPoint/eventAoi add target_relation, so a scene that only clips the bbox is not read as covering the event.
Search the imagery catalog (Sentinel-1, Sentinel-2, NISAR L-band) over an area and window. Metadata only, no pixels. With eventDate each scene is tagged timing=pre/post/same_day_unknown — **a same-day scene is same_day_unknown, never post**, without a real event time — and meta reports bracketing and sar_pair_status. eventPoint/eventAoi add target_relation, so a scene that only clips the wide bbox is not read as covering the event.
| Parameter | Type | Req. | Constraints | Description |
|---|---|---|---|---|
| bbox | number[] | yes | 4 items | [minLon, minLat, maxLon, maxLat] WGS84. Required. |
| collection | string | no | sentinel-1-grd | sentinel-1-rtc | sentinel-2-l2a | NISAR_L2_GCOV_PROVISIONAL_V1 | Catalog collection. Default sentinel-2-l2a. |
| startDate | string | no | — | Date range start, YYYY-MM-DD (UTC). With endDate; omit both for the last 7 days (max 30). |
| endDate | string | no | — | Date range end, YYYY-MM-DD (UTC), inclusive. |
| eventDate | string | no | — | Widens to the canonical pre/post span with bracketing; sentinel-1-grd adds sar_pair_status. |
| eventPoint | number[] | no | 2 items | [lon, lat] WGS84. Drives covers_event_point / usable_for_event. |
| eventAoi | number[] | no | 4 items | [minLon,minLat,maxLon,maxLat]. Drives intersects_event_aoi / coverage_ratio. |
| eventTimestamp | string | no | — | ISO 8601 event time — promotes same-day scenes to pre/post. |
| cloudCoverMax | number | no | 0…100 | Sentinel-2 only: max cloud cover %. |
| limit | integer | no | 1…25 | Max scenes. Default 10. |
Result (structuredContent)
| Field | Type | Always | Description |
|---|---|---|---|
| meta | object | yes | Query echo, token charge/balance (meta.tokens), and pagination where applicable. |
| summary | string | no | One-line natural-language summary of the result, ready to relay to a user. |
| scenes | object[] | yes | |
| full_records | object | no | The same query on the REST API and the Python SDK, which return every field and larger pages. |
plan_event_imageryMetered · 4 tokThe deterministic imagery plan for ONE event: checks BOTH sensors exactly once — sentinel-1-grd (SAR, the only look that survives cloud and night) and sentinel-2-l2a — against the event footprint and a pre/post window. **A VHR recommendation never invalidates what the free catalog showed.**
The deterministic imagery plan for ONE event: the server checks BOTH sensors exactly once — sentinel-1-grd (SAR, the only look that survives cloud and night) and sentinel-2-l2a — against the event footprint and a pre/post window, reporting how many scenes COVER the event and are usable, SAR pair status and bracketing. **A VHR recommendation never invalidates what the free catalog already showed.** An event with no resolvable footprint is refused, not guessed.
| Parameter | Type | Req. | Constraints | Description |
|---|---|---|---|---|
| event_id | string | yes | — | The `id` (UUID) from query_signals; the server resolves its point and AOI. |
| analysis_goal | string | yes | damage_assessment | flood_mapping | wildfire_assessment | What the imagery must establish — decides the lead collection and cloud gating. |
| event_date | string | no | — | YYYY-MM-DD. The event row supplies it when known. |
Result (structuredContent)
| Field | Type | Always | Description |
|---|---|---|---|
| meta | object | yes | Query echo, token charge/balance (meta.tokens), and pagination where applicable. |
| summary | string | no | One-line natural-language summary of the result, ready to relay to a user. |
| plan | object | yes |
rank_imaging_priorityMetered · 1 tokWHERE, and with what class of satellite, observation is most worthwhile now: composite IMPORTANCE crossed with the SPEC CLASS the required resolution demands — coarse, hr (free Sentinel-class) or vhr. Deterministic.
WHERE, and with what class of satellite, observation is most worthwhile now. Crosses each event's composite IMPORTANCE with the SPEC CLASS its resolution demands: coarse (<=100 m), hr (<=10 m, free Sentinel-class) or vhr (sub-metre, commercial). Deterministic, no LLM.
| Parameter | Type | Req. | Constraints | Description |
|---|---|---|---|---|
| bbox | number[] | no | 4 items | [west, south, east, north] WGS84. Omit for global. |
| start_date | string | no | — | YYYY-MM-DD, inclusive. Default today. Records start 2026-09-23 (earlier: unrecorded, not quiet). Plan-bounded: meta.window_clamp. |
| end_date | string | no | — | YYYY-MM-DD, inclusive. Default today; capped at 30 days. |
| categories | string[] | no | — | Restrict to these Delta categories. |
| min_geoint_score | number | no | — | Drop events below this GEOINT score before ranking. |
| top_n | number | no | 1…50 | How many top targets to return (default 12). |
Result (structuredContent)
| Field | Type | Always | Description |
|---|---|---|---|
| meta | object | yes | Query echo, token charge/balance (meta.tokens), and pagination where applicable. |
| summary | string | no | One-line natural-language summary of the result, ready to relay to a user. |
| priority | object | yes |
predict_satellite_passesMetered · 2 tokWHEN a place can next be imaged and by WHAT: 13 free-systematic and commercial-taskable families. **Every pass is a GEOMETRIC access opportunity** — no operator plan is consulted. retrieval_ok:false means timing is UNAVAILABLE, never "no passes".
WHEN a place can next be imaged and by WHAT. SGP4 over day-cached elements for 13 families: free SYSTEMATIC (Sentinel-1/2, Landsat, NISAR) and commercial AGILE (WorldView, ICEYE, Capella, SkySat, Umbra, Synspective, iQPS, RADARSAT-2, COSMO-SkyMed — taskable access needing a paid order, NOT a guaranteed collect). **Every pass is a GEOMETRIC access opportunity** — no operator plan is consulted. retrieval_ok:false means timing is UNAVAILABLE, never "no passes".
| Parameter | Type | Req. | Constraints | Description |
|---|---|---|---|---|
| lat | number | no | -90…90 | Target latitude. Required unless bbox is given. |
| lon | number | no | -180…180 | Target longitude. Required unless bbox is given. |
| bbox | number[] | no | 4 items | [west, south, east, north] WGS84. Its CENTRE is the target if lat/lon are omitted. |
| start_date | string | no | — | YYYY-MM-DD (UTC), inclusive. Default today. |
| end_date | string | no | — | YYYY-MM-DD (UTC), inclusive. Default start+2 days; 7-day horizon. |
| satellites | string[] | no | sentinel-1 | sentinel-2 | landsat | worldview | iceye | capella | skysat | umbra | synspective | iqps | radarsat-2 | cosmo-skymed | nisar | Families to consider. Omit for all thirteen. |
| max_passes | number | no | 1…100 | Maximum passes to return, soonest first (default 40). |
Result (structuredContent)
| Field | Type | Always | Description |
|---|---|---|---|
| meta | object | yes | Query echo, token charge/balance (meta.tokens), and pagination where applicable. |
| summary | string | no | One-line natural-language summary of the result, ready to relay to a user. |
| passes | object[] | yes | |
| freshness | object | no |
assess_signalMetered · 5/15 tokAI remote-sensing deep-dive for one signal: what to observe, sensors, a collection window. Also returns a deterministic `context` whose `imagery_handoff.parameters` are the exact search_imagery inputs. Cached per signal; not-observable signals are rejected before any charge.
AI remote-sensing deep-dive for one signal: what to observe, recommended sensors, a collection window. Also returns a deterministic `context` whose `imagery_handoff.parameters` are the exact search_imagery inputs for real pre/post candidates. Cached per signal, not re-charged. Signals that are not satellite-observable are rejected BEFORE any charge.
| Parameter | Type | Req. | Constraints | Description |
|---|---|---|---|---|
| eventId | string | yes | — | Signal id (UUID) from query_signals. |
| kind | string | no | quick | deep | Assessment depth. Defaults to quick. |
Result (structuredContent)
| Field | Type | Always | Description |
|---|---|---|---|
| kind | string | no | Assessment depth actually run ("quick" | "deep"). |
| cached | boolean | no | True when a prior assessment for this signal was reused (no re-charge). |
| model | string | no | |
| content | object | no | |
| meta | object | no | Query echo, token charge/balance (meta.tokens), and pagination where applicable. |
| context | object | no | Deterministic collection context for this signal — not model prose. Use `imagery_handoff` to go straight from the assessment to real scene candidates. |
| status | string | no | Present ONLY on a pre-charge rejection; `content` and `meta` are then absent. |
| event_id | string | no | Echoed on a rejection so the caller can tell which signal it was. |
| charged | integer | no | Tokens charged on a rejection — always 0. |
| reason | string | no | Why the signal was rejected before any charge. |
| reason_codes | string[] | no |
ask_analystMetered · 5–123 tokAsk the Delta Analyst an OSINT/GEOINT question; returns a structured brief. **Durable async**: {status:"processing", job_id} comes back immediately and the run finishes in a background worker. Fetch with get_analyst_job, or re-send the SAME idempotencyKey (no second charge).
Ask the Delta Analyst an OSINT/GEOINT question. Agentic multi-step analysis returning a structured brief (summary, findings with collection recommendations, assessment, citations). **Durable async**: returns {status:"processing", job_id} immediately and finishes in a background worker, so it survives a client timeout. Fetch with get_analyst_job, or re-send the SAME idempotencyKey (no second charge).
| Parameter | Type | Req. | Constraints | Description |
|---|---|---|---|---|
| question | string | yes | — | The analytic question (≤ 500 chars). |
| bbox | number[] | no | 4 items | Focus bbox [minLon, minLat, maxLon, maxLat] WGS84. |
| mode | string | no | fast | deep | fast (default) or deep. Deep widens reasoning and gathering: slower, ceiling 123→415, still charged by usage. |
| idempotencyKey | string | no | — | At-most-once key. Re-sending the SAME key returns the SAME run with no second charge, so a timeout is recoverable. |
Result (structuredContent)
| Field | Type | Always | Description |
|---|---|---|---|
| brief | object | no | |
| meta | object | no | Query echo, token charge/balance (meta.tokens), and pagination where applicable. |
| status | string | no | "processing" when the run is still going (poll get_analyst_job or re-send the same idempotencyKey). |
| job_id | string | no | Id of the analyst run — pass to get_analyst_job (also at GET /api/v1/analyst/{job_id}). |
| progress | object | no | Pipeline progress while the job is processing. completed_steps reaches total_steps ONLY when status is "done". |
| estimated_charge | object | no | The charge ceiling quoted for THIS run, fixed at enqueue (統合改善指示書 P1-1). The completed run reports the actual charge in meta.tokens.charged and echoes this ceiling as meta.tokens.maximum_promised; actual never exceeds it. |
| message | string | no |
get_analyst_jobFreeStatus and result of an ask_analyst run. Status is "processing", "done" or "error"; result_quality.status is **SEPARATE** — a done job can carry a partial result.
Status and result of an ask_analyst run by job_id. Status is ONLY "processing", "done" or "error". result_quality.status ("complete" | "partial") is **SEPARATE from job status**: a done job can carry a partial result. Visible only to the key that created it.
| Parameter | Type | Req. | Constraints | Description |
|---|---|---|---|---|
| job_id | string | yes | — | The job_id returned by ask_analyst. |
Result (structuredContent)
| Field | Type | Always | Description |
|---|---|---|---|
| job_id | string | no | |
| status | string | no | "processing" | "done" | "error". |
| progress | object | no | Pipeline progress while the job is processing. completed_steps reaches total_steps ONLY when status is "done". |
| brief | object | no | |
| meta | object | no | Query echo, token charge/balance (meta.tokens), and pagination where applicable. |
| error | string | no | Failure reason when status is "error". |
| message | string | no | |
| created_at | string | no | |
| updated_at | string | no |
query_developmentsMetered · 3 tokWhat actually CHANGED about the events in an area, not which articles are new. Each change is labelled `world` (the event's own state moved) or `measurement` (what we can see moved). Plan-dependent values say so (`locked_by`).
What actually CHANGED about the events in an area, not which articles are new. Each change carries what it was and what it became, labelled `world` (the event's own state moved) or `measurement` (what WE can see moved: location resolved, imagery arrived, SAR pair ready). The right source for "what is new since last time".
| Parameter | Type | Req. | Constraints | Description |
|---|---|---|---|---|
| bbox | number[] | no | 4 items | [minLon, minLat, maxLon, maxLat]. Omit for worldwide. |
| start_date | string | no | — | Date range start (YYYY-MM-DD). With end_date; omit both for today. Records start 2026-09-23 (earlier: unrecorded, not quiet). Plan-bounded: meta.window_clamp. |
| end_date | string | no | — | Date range end (YYYY-MM-DD), inclusive. |
| categories | string[] | no | — | Restrict to these signal categories. |
| axes | string[] | no | attributed_actor | casualties_disputed | casualties_injured | casualties_killed | distinct_hosts | escalation_trend | geoint_score | location_level | means_reported | member_count | occurred_at | point_plottable | quality_status | severity_score | stage | target_effect | target_status | targets_named | Restrict to these axes. An unknown value errors rather than returning nothing. |
| notable_only | boolean | no | — | Default true. False returns every recorded change. |
| include_member_count | boolean | no | — | Default false. "One more report arrived" is not news about the event. |
| limit | number | no | 1…50 | Developments to return (default 20). |
| offset | number | no | 0…∞ | Skip this many; meta.total_count is the window. |
Result (structuredContent)
| Field | Type | Always | Description |
|---|---|---|---|
| summary | string | no | One-line natural-language summary of the result, ready to relay to a user. |
| meta | object | no | Query echo, token charge/balance (meta.tokens), and pagination where applicable. |
| developments | object[] | yes |
get_event_threadFreeOne event end to end: state plus every change in order — the "new event or update" distinction a feed cannot make. `occurred_at_basis` distinguishes stated, absent and never measured. Plan-dependent: see `locked_by`, `plan_lock`.
One event end to end: current state and every change in order — the "new event or update to an old one" distinction a feed cannot make. Returns the canonical event (where, when it HAPPENED as distinct from when it was reported, casualties, attribution and its grounding), a timeline and every source. `occurred_at_basis` distinguishes stated, absent and never measured.
| Parameter | Type | Req. | Constraints | Description |
|---|---|---|---|---|
| event_id | string | yes | — | Event id (UUID). A folded id resolves to its event; merged_from_request says so. |
Result (structuredContent)
| Field | Type | Always | Description |
|---|---|---|---|
| summary | string | no | One-line natural-language summary of the result, ready to relay to a user. |
| canonical_event | object | yes | |
| timeline | object[] | yes | |
| sources | object[] | no |
list_watchesFreeThe Watchlist as one list, each entry with a state bucket. `updated_since` returns only watches whose CONTENT changed after that instant — what a synchronised copy should poll.
The Watchlist: measured areas, event-watched areas and tracked events as one list, each with a state bucket (needs_attention, changed_today, awaiting_data, stable). A watch groups everything on the same target. `updated_since` returns only watches whose CONTENT changed after that instant — what a synchronised copy should poll.
| Parameter | Type | Req. | Constraints | Description |
|---|---|---|---|---|
| updated_since | string | no | — | ISO 8601. Only watches whose CONTENT changed after it — the evidence, not renames. |
| cursor | string | no | — | Cursor from meta.next_cursor. |
| limit | integer | no | 1…500 | Watches per page when paging (1..500, default 100). |
Result (structuredContent)
| Field | Type | Always | Description |
|---|---|---|---|
| summary | string | no | One-line natural-language summary of the result, ready to relay to a user. |
| watches | object[] | yes | |
| buckets | object | no | |
| limits | object | no | |
| meta | object | no |
list_notificationsFreeWhat changed across the Watchlist, newest first: one row per change a watch reported, with read state. Reading does not mark them read. A value the plan does not open is `locked`, never filled in.
The Notification Center: every change a watch reported — an event development, a newly related event, an anomaly in a measurement, a new event at a site, a new scene, a fired standing order — one row each, newest first, the same rows as the bell in the app and the Watch update emails. `unread_count` in meta is the total unread regardless of filters. **Listing does not mark anything read**: the unread state belongs to the user's inbox, and a conversation reading it should not clear the bell. Marking read is POST /api/v1/notifications/read or client.notifications.mark_read in the Python SDK — deliberately not an MCP tool (inbox housekeeping, not something a conversation needs). A value the plan does not open is withheld: `locked` is true and `lock_plan` names the plan that opens it. For one watch in full, follow watch.id into get_watch.
| Parameter | Type | Req. | Constraints | Description |
|---|---|---|---|---|
| unread_only | boolean | no | — | Only notifications not yet read. |
| kind | string | no | event_development | related_event | monitoring_anomaly | site_event | new_imagery | standing_order | |
| watch_id | string | no | — | Id from list_watches. |
| cursor | string | no | — | Cursor from meta.next_cursor. |
| limit | integer | no | 1…25 | Rows per page (1..25, default 10). |
Result (structuredContent)
| Field | Type | Always | Description |
|---|---|---|---|
| summary | string | no | One-line natural-language summary of the result, ready to relay to a user. |
| notifications | object[] | yes | |
| meta | object | no | |
| full_records | object | no | The same query on the REST API and the Python SDK, which return every field and larger pages. |
get_watchFreeOne watch end to end: target, state, latest change, measurements with recent series, standing-order questions, and for an event watch its stage, developments, imagery check and thread, plus this account's notes.
One watch end to end: target, state, latest meaningful change, measurements with recent series, standing-order questions, and for an event watch its stage (how precisely it is located), developments, imagery availability and full thread — plus this account's notes and judgments. An event watch reports signal.imagery_check: whether imagery is being checked for it and, if not, why (see create_watch for the rule and the charge). The FULL measurement history is on the REST API (GET /api/v1/monitoring/{areaId}) and the Python SDK (client.monitoring.get).
| Parameter | Type | Req. | Constraints | Description |
|---|---|---|---|---|
| watch_id | string | yes | — | Id from list_watches. |
| include_passes | boolean | no | — | Add the collection outlook. Costs the same as predict_satellite_passes; omit it and the call is free. |
Result (structuredContent)
| Field | Type | Always | Description |
|---|---|---|---|
| summary | string | no | One-line natural-language summary of the result, ready to relay to a user. |
| watch | object | yes | |
| thread | object | no |
create_watchFreeAdd a target to the Watchlist. EVENT: the signal's event id binds the canonical event — **never watch an article URL**. AREA: a bbox. SITE: entity_id or lat+lon, and scale. Free to create; where the plan includes the collection section, a watched event's imagery is then checked at 1 token a day (up to 14 days).
Add a target to the Watchlist. EVENT: pass the signal's event id and the server binds the canonical event, so later reporting and even a cluster merge stay on the same watch — **never watch an article URL**. AREA: pass a bbox of at most 100,000 km² (larger is refused with 400 invalid_input; for a whole country use SITE at scale "country"); measuring there and a standing order on its events are created over the REST API or the Python SDK (monitoring / standing-orders) and auto-join the same watch. SITE: watch a specific place for change — a registry facility (entity_id from search_entities) or any point (lat, lon) — at a scale: "facility" (radius 250 m–3 km; measured from imagery by default when the kind of facility has a suggested measurement, e.g. vessel counts for a port), "city" (5–20 km; measured only when you pass a metric) or "country" (reporting only; never measured). Linked events come back with how they were linked (named / nearby / country). A refused explicit metric creates nothing; a refused default is reported in measurement.reason. Measuring uses a monitored-area slot and costs the same as a monitored area. IMAGERY FOR A WATCHED EVENT: creating the watch is free. Where the account's plan includes the collection section (the imaging recommendation), imagery of a watched event is then checked automatically at 1 token a day per watch, until before and after scenes are found or 14 days after the first report; a day without enough tokens is skipped, not charged. On other plans imagery is not checked and nothing is charged. Imagery is checked only for watched events — an event that is not watched carries no imagery status.
| Parameter | Type | Req. | Constraints | Description |
|---|---|---|---|---|
| target_type | string | yes | event | area | site | |
| event_id | string | no | — | EVENT: signal uuid. |
| bbox | number[] | no | 4 items | AREA: [W,S,E,N] WGS84, at most 100,000 km² (larger → 400). |
| entity_id | string | no | — | SITE: registry id |
| lat | number | no | — | |
| lon | number | no | — | |
| scale | string | no | facility | city | country | |
| radius_m | number | no | — | |
| metric | string | no | — | SITE: metric or "none". |
| name | string | no | — | Label. |
| notify_email | boolean | no | — | Email on changes. |
| imagery_alerts | boolean | no | — | AREA/SITE: new-scene alerts. |
Result (structuredContent)
| Field | Type | Always | Description |
|---|---|---|---|
| summary | string | no | One-line natural-language summary of the result, ready to relay to a user. |
| watch | object | yes | |
| already_existed | boolean | no |
update_watchFreePause, resume or close a watch. **Pausing is not a display state**: monitored areas stop being measured and charged, standing orders stop checking. **Closing states how the question ended** (close_reason).
Pause and resume a watch, CLOSE it when the question is settled, or turn on `imagery_alerts`. **Closing states how the question ended** and requires `close_reason`: `resolved` (an answer was reached), `lapsed` (interest moved on without an answer) or `false_alarm` (never a watchable event). A closed watch stops notifying, stops its bound measurements and standing orders, and frees an active slot — so close rather than delete when the record is worth keeping. **Pausing is not a display state over live billing**: monitored areas stop being measured and charged, standing orders stop checking. Resuming restarts them, standing orders due immediately. Use it instead of delete_watch when the history is worth keeping. `imagery_alerts` is for AREA watches and facility- or city-scale SITE watches: it reports that a NEW scene now covers the area — availability, never what is in the scene. For an event, post-event coverage already arrives as a development on the event itself. `notify_chat` posts the watch’s notifications to the Slack or Discord channel the user connected in account settings (connecting happens in the app); with none connected the setting is kept and nothing is posted.
| Parameter | Type | Req. | Constraints | Description |
|---|---|---|---|---|
| watch_id | string | yes | — | Id from list_watches. |
| status | string | no | active | paused | saved | closed | "paused" stops the bound checks; "active" resumes them; "closed" ends the question and needs close_reason. |
| close_reason | string | no | resolved | lapsed | false_alarm | Required when status is "closed". resolved = an answer was reached; lapsed = interest moved on without an answer; false_alarm = never a watchable event. They point at different things to fix, so do not collapse them. |
| notify_email | boolean | no | — | Email on changes. |
| notify_chat | boolean | no | — | Also post changes to the Slack/Discord channel connected in settings. |
| imagery_alerts | boolean | no | — | AREA/SITE: new-scene alerts. |
Result (structuredContent)
| Field | Type | Always | Description |
|---|---|---|---|
| summary | string | no | One-line natural-language summary of the result, ready to relay to a user. |
| watch | object | no |
delete_watchFreeDelete a watch and its underlying resources — bound monitored areas with their history, and standing orders. To stop without losing them, pause with update_watch.
Delete a watch and its underlying resources — bound monitored areas with their measurement history, and standing orders — exactly as the in-app Watchlist does. To stop without losing anything, pause with update_watch instead.
| Parameter | Type | Req. | Constraints | Description |
|---|---|---|---|---|
| watch_id | string | yes | — | Id from list_watches. |
Result (structuredContent)
| Field | Type | Always | Description |
|---|---|---|---|
| summary | string | no | One-line natural-language summary of the result, ready to relay to a user. |
| deleted | string | no | |
| removed | object | no |
add_noteFreeAdd a note to a watch, start a thread, or reply. **Confidence is what separates a note from a judgment**: with one it is a JUDGMENT that supersedes the previous judgment rather than overwriting it.
Add a note to a watch, start a thread, or reply — one append-only ledger. A title makes it a thread; replies are one level deep. **Confidence is what separates a note from a judgment**: with one it is a JUDGMENT that supersedes the previous judgment rather than overwriting it. It does not change the event's public record.
| Parameter | Type | Req. | Constraints | Description |
|---|---|---|---|---|
| watch_id | string | yes | — | Id from list_watches. |
| note | string | yes | — | What you want kept with this watch. |
| title | string | no | — | A heading makes it a thread. A reply cannot have one. |
| parent_id | string | no | — | Reply to this note id. One level deep. |
| confidence | string | no | high | moderate | low | How sure the judgment is — about the evidence, not the event. Omit and it stays a note. |
| likelihood | string | no | very unlikely | unlikely | roughly even chance | likely | very likely | almost certain | How probable the thing itself is (ICD 203). Omit rather than guess. |
| gaps | string[] | no | — | What would change this judgment. Only meaningful with a confidence. |
| next_check | string | no | — | When/what to look at next. |
Result (structuredContent)
| Field | Type | Always | Description |
|---|---|---|---|
| summary | string | no | One-line natural-language summary of the result, ready to relay to a user. |
| note | object | yes |
search_entitiesMetered · 1 tokFind a named place (airport, base, plant, port, dam, strait…) in any language or by IATA/ICAO code. Max 50.
Find a place in the location registry — airports, military and naval bases, launch sites, nuclear facilities, ports, power plants, dams, bridges, government sites, chokepoints and named seas. Matches names in many languages and IATA/ICAO codes, most prominent first. At most 50 rows, no pagination. Cite the returned attribution.
| Parameter | Type | Req. | Constraints | Description |
|---|---|---|---|---|
| query | string | yes | — | Name or partial name (at least 2 characters). |
| subtypes | string[] | no | port_facility | military_base | airport | refinery_energy | nuclear_facility | launch_site | dam | bridge | government_site | chokepoint | water_body | Kinds to keep. Omit for all. |
| limit | number | no | — | Max rows (1-50, default 20). |
Result (structuredContent)
| Field | Type | Always | Description |
|---|---|---|---|
| summary | string | no | One-line natural-language summary of the result, ready to relay to a user. |
| entities | object[] | yes | |
| limit | number | no |
get_entityMetered · 1 tokWhat has happened at one place: the registry record plus its most recent linked events, each with HOW it was linked. **`link_kind: proximity` is an association, not a statement that the event happened there.**
What has happened at one place: the registry record plus its most recent linked events (`event_count` is the total), each carrying HOW it was linked — `link_kind` `named_location` (a report named this place as where it happened), `named_target` (named it as the target), `measured` (a satellite measurement recorded near it, with `distance_m` — what was measured, not a report of what happened) or `proximity` (located within range, with `distance_m`). **Read the basis: proximity is an association, not a statement that the event happened there.**
| Parameter | Type | Req. | Constraints | Description |
|---|---|---|---|---|
| entity_id | string | yes | — | The id from search_entities. |
Result (structuredContent)
| Field | Type | Always | Description |
|---|---|---|---|
| summary | string | no | One-line natural-language summary of the result, ready to relay to a user. |
| entity | object | yes | |
| events | object[] | no | |
| event_count | number | no | |
| events_24h | number | no |
get_related_eventsMetered · 1 tokThe relations recorded for one event: others at the same registry facility, naming the same place, or stored as the same campaign, each saying what is shared; plus reports merged into it. Merely-nearby events are not relations and are not returned. Plan-dependent: see `plan_lock`.
The relations recorded for one event — nothing is inferred on request. **Every entry in `related` says what the two events share** in `shares`: the same registry facility, the same named place (resolved to a town or finer), or a relation stored when the events were processed (`same_campaign`). Reports of the same event merged into it are in `merged_in`. Events that are merely close in space and time are not relations and are not returned. Each facility and place lists its most recent other events; `other_count` is the total.
| Parameter | Type | Req. | Constraints | Description |
|---|---|---|---|---|
| event_id | string | yes | — | The event id (a UUID) from query_signals. |
Result (structuredContent)
| Field | Type | Always | Description |
|---|---|---|---|
| summary | string | no | One-line natural-language summary of the result, ready to relay to a user. |
| anchor | object | no | |
| facilities | object[] | no | |
| places | object[] | no | |
| related | object[] | yes | |
| merged_in | object[] | no | |
| accounting | object | no |
REST ↔ MCP parameters
The MCP tools mirror the REST API — same filters, same token costs. Some argument names differ: REST uses snake_case query params while several MCP arguments are camelCase (the table lists them), and a bounding box is a comma-string in REST but a 4-number array in MCP.
| REST (query param) | MCP (argument) |
|---|---|
| cloud_cover_max | cloudCoverMax |
| end_date | endDate |
| entityId | entity_id |
| event_aoi | eventAoi |
| event_date | eventDate |
| event_point | eventPoint |
| event_timestamp | eventTimestamp |
| eventId | event_id |
| Idempotency-Key | idempotencyKey |
| include_member_count | includeMemberCount |
| jobId | job_id |
| linked_to | linkedTo |
| min_publishers | minPublishers |
| min_severity_band | minSeverityBand |
| q | query |
| start_date | startDate |
| updated_since | updatedSince |
| watch_id | watchId |
| watchId | watch_id |
| bbox (comma string) | bbox (number[4]) |
Example response
query_signals over MCP (digest rows; values are illustrative). Full field definitions are in the signals://schema resource.
{
"meta": {
"start_date": "2026-09-28",
"end_date": "2026-09-29",
"count": 20,
"total_count": 1181,
"next_cursor": "My5tWWpiLXU1YWg4UWVRTU5E",
"has_more": true,
"tokens": {
"charged": 3,
"remaining": 6836
}
},
"summary": "20 signal(s) 2026-09-28→2026-09-29 (more pages available). Top categories: kinetic (8), armed_conflict (5).",
"signals": [
{
"id": "67f0dcc5-19e4-4a38-8c7c-fc3fa1ad4513",
"title": "Strike reported near a port",
"category": "kinetic",
"summary": "An airstrike hit an apartment building in the port district; officials report casualties…",
"last_reported_at": "2026-09-29T07:45:00+00:00",
"country": "Example Country",
"location": "port district",
"lat": 31.44,
"lng": 34.36,
"stage": "reported",
"severity_band": 7,
"geoint_score": 9,
"escalation_trend": "escalating",
"distinct_hosts": 1,
"collection": {
"sensor": "vhr-optical",
"level": "GSD<1m",
"observable": false
},
"plan_lock": null
}
],
"full_records": {
"note": "Rows here are digests. get_event_thread gives one event in full; the REST API and the Python SDK return every field and up to 500 rows per page.",
"rest": "GET https://offnadir-delta.com/api/v1/signals?recency=24h",
"sdk": "client.signals.list(recency=\"24h\")",
"docs": "https://offnadir-delta.com/docs/api#ep-signals"
}
}Resources
| brief://latest | The most recent AI-synthesized Daily World Brief (JSON). Free. |
| signals://schema | JSON Schema of the public Signal shape returned by query_signals / /api/v1/signals. |
| usage://current | Remaining token balance and plan capabilities for the calling key. Free. |
| status://current | How current the data is (ingestion/enrichment frontier), the Daily World Brief status, and an Operational/Delayed/Degraded roll-up. Free. |
| brief://{date} | The Daily World Brief for a specific UTC date (YYYY-MM-DD). Free. |
| watch://{watch_id} | A Watchlist entry with its current state, latest meaningful change, measurements, (for event watches) the full event thread, and the notes kept against it. The same body get_watch returns. Free. |
Looking for the underlying REST endpoints, parameters, and token costs? See the API reference.
Frequently Asked Questions
How is MCP different from the REST API and the SDK?▼
Is it authenticated and metered?▼
Why does my client still list tools that are not on this page?▼
From headline to satellite evidence
One connected intelligence workflow across four surfaces — free to start, no GIS software or remote-sensing background required.