One picture. Every feed. Agent-callable.

A resolved-canonical-entity ontology fuses TAK, ADS-B, ASTERIX, AIS, and Remote ID into one typed object graph — so the same helicopter heard on three feeds is one entity, not three. Then an in-process MCP (Model Context Protocol) server exposes that graph as tools an AI agent can call directly: resolve_entity, find_paths, get_neighbors. The agent reasons over structured objects, not hand-written SQL — running entirely on your hardware.

Entity ResolutionTyped Object GraphMCP Tools Find PathsOAG GroundingStreamable HTTP100% local
The resolution problem

The same platform on three feeds is three nodes — until it isn't.

Every legacy link graph treats each feed-native id (a CoT uid, an ICAO24 hex, an MMSI) as a separate node. So an aircraft heard simultaneously on TAK, ADS-B, and ASTERIX shows up three times with no connection between them. The ontology fixes this with deterministic cross-feed resolution: a shared key (ICAO24 for crewed aircraft, Operator+Serial for drones, MMSI for vessels, CoT Uid for ground units) collapses every observation of the same platform into one canonical entity.

  • Canonical entities — typed Object Types (Person, Vehicle, Aircraft, Vessel, UAS, Organization, Place, Event, Mission) with stable ids that survive feed renames and callsign changes.
  • The alias seam — every feed-native id maps to a canonical entity via ResolveByAlias(source, nativeId), the single lookup the resolver does on every observation.
  • Persistence — a GeoPackage at <dataDir>/gpkg/ontology.gpkg holds entities, links, aliases, and embeddings; survives restarts, opens in QGIS/ArcGIS Pro.
  • Latest-position merge — identity fields come from the highest-priority source; dynamic position always overwritten by the freshest sighting.
resolution keys · deterministic
# cross-feed fusion by shared key
Aircraft (crewed) → ICAO24
   joins ASTERIX CAT021/062 + ADS-B + CoT
UAS (drones)      → OperatorId:Serial
   joins RemoteID + SDR UAS fields
Vessels           → MMSI
   AIS only (no cross-feed merge partner)
Ground / Person   → CoT Uid
   TAK-native
Organization      → op:<operatorId>

● no fuzzy matching · no false positives
The MCP tool surface

The agent calls the graph, not SQL.

An in-process MCP server (official ModelContextProtocol SDK, Streamable HTTP, Stateless mode) exposes the ontology as eight typed tools at /mcp on the existing Kestrel. Cursor, Claude Desktop, or your own automation gets structured JSON it can reason over.

🔗

resolve_entity

Map a feed-native id (CoT uid, ICAO24 hex, MMSI) to its canonical entity id. The single lookup that powers every downstream operation.

🔎

get_entity

Fetch an entity plus its full alias history — which feeds saw it, when, the provenance/lineage view in one call.

🕸️

get_neighbors

The "Search Around" / Expand operation — a canonical entity's 1-to-3-hop ego-network (nodes + edges).

🛣️

find_paths

Shortest paths between two entities — the Palantir "Find Paths". Pure C# BFS over the persisted links, bounded by maxHops.

📈

pagerank

Centrality over the entity graph (onager when available; degree-centrality fallback) — find the hubs.

👥

louvain

Community detection — cluster the graph into neighborhoods to surface operational groupings.

OAG — Ontology-Augmented Generation

Ground the LLM in typed objects, not text chunks.

The Palantir AIP insight, copied locally: instead of naive RAG (hand the LLM raw document snippets), retrieve typed, governed ontology objects and their links and inject them as structured context. The LLM reasons over explicit entities and relations rather than re-deriving them from prose. Two-tier retrieval: deterministic ID extraction (callsign / ICAO hex / MMSI regex → alias lookup) plus optional embeddings cosine search when the sidecar is up.

  • Off by default — an "🧠 OAG context" toggle in the AI panel; the operator opts in. Zero behavior change unless turned on.
  • Graceful degrade — embeddings sidecar down? Falls back to keyword/ID-only retrieval. No sidecar, no problem.
  • Structured context — each retrieved entity arrives with its aliases, position, and 1-hop links so the LLM sees the relational neighborhood.
/mcp · Streamable HTTP
# the 8 typed tools
resolve_entity   → {entityId}
get_entity       → entity + aliases
list_entities    → filtered by type
get_neighbors    → ego-network
find_paths       → shortest paths
pagerank         → centrality
louvain          → communities
summary          → counts + breakdown

# one write tool, gated
merge_entities   → AllowWrites=false by default

● read-only by default · loopback only

The MCP endpoint is loopback-only by default (the host binds IPAddress.Loopback). The existing ApiKeyMiddleware covers /api/*; /mcp is off unless Mcp:Enabled=true in appsettings.json. LAN exposure + MCP OAuth is a documented follow-up. The ontology schema is a Phase-1 static type registry; runtime-mutable "Dynamic" schema (Palantir's pitch) and probabilistic matching are deferred — deterministic keys + a manual merge action handle the realistic cases without the false-positive risk.

The COP, queryable by an agent.

Resolve across feeds, store as typed objects, expose as tools — and a connected AI agent can ask "who has N12345 been co-located with?" and get the resolved entity's neighborhood back as structured data. All on your hardware, no cloud round-trip.