mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
feat: add a tool catalog and local MCP server
Define tools that answer questions from the app's data once, independent of the surface that exposes them. A tool declares its arguments and the services it reads; applications compose tools with their own services and optional call interceptors. Queries reuse the layers' portable source factories, so they run wherever an application routes /api requests. The first queries are get_earthquakes, get_active_fires and get_recent_launches, with a shared area argument (place name, bounding box, or point and radius) and capped lists. gods-eye-view/tools/mcp adapts a catalog to the Model Context Protocol with a small JSON-RPC handler and a stateless HTTP transport, adding no dependencies. npm run mcp serves Core's tools over stdio to local clients such as Claude Code, reading from a running app. Both exports are portable and checked by check:boundaries; the app does not import them.
This commit is contained in:
1 parent
e7707d9a0f
commit
c3dfa8e853
26 files changed
+1886
-3
No files matched your search
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* A `places` service backed by the app's `/api/geocode` route. Applications
|
||||
* with a different place search can supply any object with the same
|
||||
* `resolve(name, { signal })` contract.
|
||||
*/
|
||||
|
||||
const POINT_RADIUS_DEGREES = 0.25;
|
||||
|
||||
/** Resolve place names to `{ name, bounds: { west, south, east, north } }` or null. */
|
||||
export function createGeocodePlaceService({
|
||||
fetchImpl = (...args) => globalThis.fetch(...args),
|
||||
} = {}) {
|
||||
return {
|
||||
async resolve(name, { signal } = {}) {
|
||||
const query = new URLSearchParams({ q: name });
|
||||
const response = await fetchImpl(`/api/geocode?${query}`, { signal });
|
||||
if (!response.ok) throw new Error(`Geocode HTTP ${response.status}`);
|
||||
const payload = await response.json();
|
||||
return placeFromGeocodeResult(payload?.results?.[0]);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Convert one geocode result to a place with bounds, or null. */
|
||||
export function placeFromGeocodeResult(result) {
|
||||
const location = result?.geometry?.location;
|
||||
if (!Number.isFinite(location?.lat) || !Number.isFinite(location?.lng))
|
||||
return null;
|
||||
const viewport = result.geometry.viewport;
|
||||
const bounds =
|
||||
viewport &&
|
||||
[
|
||||
viewport.southwest?.lat,
|
||||
viewport.southwest?.lng,
|
||||
viewport.northeast?.lat,
|
||||
viewport.northeast?.lng,
|
||||
].every(Number.isFinite)
|
||||
? {
|
||||
west: viewport.southwest.lng,
|
||||
south: viewport.southwest.lat,
|
||||
east: viewport.northeast.lng,
|
||||
north: viewport.northeast.lat,
|
||||
}
|
||||
: {
|
||||
west: Math.max(-180, location.lng - POINT_RADIUS_DEGREES),
|
||||
south: Math.max(-90, location.lat - POINT_RADIUS_DEGREES),
|
||||
east: Math.min(180, location.lng + POINT_RADIUS_DEGREES),
|
||||
north: Math.min(90, location.lat + POINT_RADIUS_DEGREES),
|
||||
};
|
||||
return { name: result.formatted_address || null, bounds };
|
||||
}
|
||||
Reference in new issue
Block a user