Skip to content

[DO NOT MERGE] - POC of MMGIS Agent Design - #229

Open
ajinkyakulkarni wants to merge 70 commits into
developmentfrom
feature/agentic-mmgis
Open

[DO NOT MERGE] - POC of MMGIS Agent Design#229
ajinkyakulkarni wants to merge 70 commits into
developmentfrom
feature/agentic-mmgis

Conversation

@ajinkyakulkarni

@ajinkyakulkarni ajinkyakulkarni commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

⚠️ DO NOT MERGE. This is a proof-of-concept opened for design review and discussion. It is feature-complete and tested, but it makes product decisions (and two small backend auth changes) that the team should weigh in on before any of it is considered for development.

What this is

A working prototype of agent-driven MMGIS: describe a spatial dashboard in plain English and get a real MMGIS mission — built from live NASA data, editable by conversation, with the agent able to drive an open map session.

Three pieces, each usable on its own:

Part What it is
mcp/ A standalone MCP server exposing 28 typed tools over MMGIS (missions, layers, geodatasets, users, STAC catalog search, live view control). Works with any MCP client — Claude Code, Claude Desktop, or the chat app below.
chat/ A self-contained chat web app. Your own OpenAI key (server-side only) drives the tools; split-panel UI shows the live dashboard beside the conversation.
AgentBridge A Plugin-Component (per spec 011) that lets the agent drive a live browser session — fly the map, toggle layers, open tools — and auto-applies config changes in modern mode.

Try it in ~5 minutes

cd mcp && npm install && npm run build      # build the MCP server
cd ../chat && npm install
cp .env.example .env                        # add OPENAI_API_KEY + MMGIS_URL/MMGIS_TOKEN
npm start                                   # → http://localhost:8895

Then ask it: "build me an air quality dashboard" → it searches NASA's VEDA catalog, lists candidate datasets with coverage, asks which you want, builds the mission, and opens it in the right-hand panel. Follow with "set the NO2 layer opacity to 0.15" or "fly the map to western Europe".

mcp/README.md and chat/README.md cover setup, every tool, and a manual E2E checklist.

How it works

browser (chat UI) ──SSE──> chat server ──stdio──> MCP server ──┬─REST──> MMGIS API (missions, layers, geodata, config generator)
                           (holds your key)      (28 tools)    └─WS────> relay → AgentBridge in the live page

Design principle throughout: wrap what MMGIS already has. Tools call the same REST endpoints the Configure page calls, shell out to the repo's own scripts/generate-mission-config.js, and ride the existing websocket relay — ~1,400 lines total. No LLM calls inside the MCP server; it exposes deterministic typed operations and the client model does the language work, which is why the same tools serve the chat app and Claude Code equally.

Please scrutinize: backend changes

The bulk of this PR is new, isolated directories. Four small changes touch existing backend code and deserve the most review attention:

  1. API/Backend/Config/routes/configs.js/api/configure/add now also accepts a SuperAdmin long-term token, not just a session. Without this, token-based mission creation is impossible. Mirrors the existing checkMissionPermission pattern.
  2. scripts/server.js — new non-blocking annotateLongTermToken middleware (validates a token if present, always calls next()), plus a .catch() on validateLongTermToken's DB query that was missing (a rejected query previously hung the request), and a shared decorateReqFromTokenData helper.
  3. API/Backend/Users/routes/users.js — the signup gate additionally accepts a SuperAdmin token. Strictly additive: requests without an Authorization header behave exactly as before.
  4. API/Backend/Utils/permissions.js (new) — shared isSuperAdminRequest(req) so the check has one definition.

Testing

  • 940 automated tests green: mcp 96, chat 30, root suite 814 (was 812 before this branch).
  • Live end-to-end against a real deployment: dashboard generation, live layer/config editing, map control, geodataset ingest → layer, mission clone, JSON round-trip, delete confirmation, token-based user creation.
  • Live LLM testing (16 real GPT-4o conversations) surfaced five bugs that unit tests could not — STAC pagination (we were reading 1 of 25 pages), a backend that reports success deleting a nonexistent mission, dropped confirm: true on retry, an inconsistent mission/missionName parameter, and hallucinated mission-name variants. All fixed and re-verified.

Known limitations (why this is a POC)

  • The websocket relay is unauthenticated upstream. Any peer that can reach it can issue view commands and read view-state acks. Mitigated here by keeping bridge commands strictly view-only and whitelist-validated in the browser; real hardening (auth, rooms, rate limits) is Phase 2.
  • /api/configure/clone and /destroy, and the /api/accounts endpoints, have no per-permission check upstream — any valid long-term token can invoke them. Pre-existing MMGIS behavior that this surface makes easier to reach; documented in mcp/README.md.
  • Embedding the dashboard in the chat panel needs X-Frame-Options relaxed; fine in dev, a deployment decision in production.
  • Concurrency is last-write-wins (single-operator assumption); mission_clone depends on a python binary on the MMGIS host.
  • Chat conversation memory is text-only across turns (tool results aren't replayed), and there's no auth/multi-user story — it's an operator tool.

Design docs

Specs and implementation plans are included under docs/superpowers/ for reviewers who want the reasoning: the umbrella design, the chat UI design, and the chat-driven-configuration design, each with its task-level plan.

Demo

MMGIS-Chat-Demo-Narrated.mp4

2:19, narrated — recorded against a live deployment: a vague "air quality dashboard" request → the agent listing real NASA datasets and asking which one → the dashboard building with every tool call visible → a live layer edit (including the agent self-correcting from an error hint) → the map flown to western Europe → the config JSON round-trip → the delete-confirmation guardrail. Ends with an animated architecture walkthrough and how the 28 tools were built.

Design for MCP-driven MMGIS: agent control surface (REST + WebSocket
agent bridge), NL-to-dashboard generation on top of the mission config
generator, and phased NL plugin scaffolding.
The /add route only checked req.session.permission, so long-term-token
requests (used by the MCP server) were always rejected with 403 even
when the token's creator was a SuperAdmin. Mirror the pattern already
used by checkMissionPermission: allow when the session permission is
111 or when req.isLongTermToken is true and req.tokenUserPermission is
111.

Also add mcp client tests for upsertMission and the transport-failure
path (fetch throws), both of which were already implemented correctly
in mmgisClient.ts.
- Add degradation hint to non-OK STAC response in stacFetch
- Encode asset, rescale, colormap in query string via encodeURIComponent
- Add tests for non-OK status hint and parameter encoding
…ing PORT

getWsPath() only checked ENABLE_MMGIS_WEBSOCKETS, unlike essence.js's bootstrap
which also gates on !isStaticBuild() and PORT being set. On a static export or
with PORT unset, AgentBridge would otherwise retry a doomed connection every
10s forever. Also adds set_time success-path coverage and broadens the
get_view_state test to cover layersOn/currentTime.
Overlapping sendCommand calls issued before the websocket finished
connecting each spawned their own WebSocket, orphaning all but the
last handle. Memoize the in-flight connect() promise on a private
connecting field so concurrent callers await the same socket.

Also adds a connect-failure test asserting the ENABLE_MMGIS_WEBSOCKETS
hint surfaces as an MMGISError.
ToolController_ (classic) never runs when a mission's msv.mode is
'modern' - only ToolControllerModern_ does, wired behind
window.mmgisAPI's show/hide/load-plugin API. commands.js was calling
the classic controller unconditionally and reporting ok:true
regardless of whether anything actually happened.

Replace the ToolController_ dependency with a mode-aware ToolAdapter
built in AgentBridge.js: classic missions still drive
ToolController_.makeTool, modern missions drive
window.mmgisAPI.showPlugin/loadPlugin based on ground-truth
isPluginLoaded/isPluginHidden state. open_tool now returns ok:false
with "Unknown or unopenable tool: <name>" whenever activation can't be
confirmed, in either mode.
dashboard_generate ran the full (expensive) profile-build and config
generation before finding out /api/configure/add would reject the
mission name. Mirror configs.js's add() character/prefix rule
(mcp/src/tools/dashboard.ts) and validate missionName first, returning
{error, hint} without touching the client. The rule is now also
documented in dashboard_profile_schema and the missionName zod
.describe().

Also: dashboard_generate silently produced a config with an unrendered
basemap when MAPBOX_TOKEN was unset. Detect whether the generated
config needed {{MAPBOX_TOKEN}} before resolving placeholders, and
surface a warnings: [...] entry in the success payload when the token
is missing.
A non-JSON response body (e.g. hitting a proxy or login page instead
of the MMGIS API) made res.json() throw a raw SyntaxError with no
actionable context. Wrap it and raise MMGISError('MMGIS returned a
non-JSON response for <path>', ...) with a hint pointing at MMGIS_URL.
JSON.parse(STAC_CATALOGS) alone doesn't guarantee the {name: url}
string map shape stac.ts assumes - an array or an object with
non-string values parsed fine but broke confusingly downstream. Add a
shape assertion and throw an existing-style Error naming the expected
shape when it doesn't hold.
The connect-phase once('error', ...) listener stays attached (it never
fired) for the lifetime of the socket, but nothing cleared this.ws
after a post-open error, so a later sendCommand could keep reusing a
dead socket. Attach a persistent on('error', ...) once the socket
opens that nulls this.ws (and this.connecting), so a later error can't
leave the client stuck and the next command transparently reconnects.
The relay note undersold the exposure: API/websocket.js broadcasts
every frame to every connected client with no per-mission routing, so
any peer can issue commands for and read view-state acks from any
mission - not just "unauthenticated upstream". Spell that out, plus
the multi-session semantics (all sessions on a mission execute each
command; first ack wins) and that presence frames are broadcast but
unconsumed (reserved for Phase 2).

Also update the view_open_tool checklist line, which hedged on
ToolControllerModern_ wiring that has since landed.
Resolve tool names to their config js id before hitting the id-keyed
mmgisAPI show/load/isPluginLoaded/isPluginHidden calls and the classic
ToolController_.toolModules/makeTool lookup, since mission configs
expose the display name while those APIs are keyed by js. Add a
dependency-free resolveToolId() in commands.js so the resolution logic
is unit-testable, and use it from both the modern and classic branches
of buildToolAdapter().

Also guard the persistent post-open websocket error handler in
mcp/src/bridge.ts so it only clears this.connecting when the erroring
socket was still the current one, preventing it from nulling out a
newer in-flight reconnect's promise.
ajinkyakulkarni and others added 29 commits July 25, 2026 15:14
Modern dashboards have no websocket client of their own (only classic
essence.js auto-applies/RELOADs on config broadcasts), so config edits
made through the MCP tools (mission_update_config, layer_add/update/
remove, tool_toggle) silently required a manual page reload to show up.
AgentBridge already holds a websocket connection in every mode, so give
it a pure, unit-tested predicate (shouldReloadForFrame) to recognize
forceClientUpdate broadcasts for the current mission and debounce a
window.location.reload() so bursts of edits coalesce into one reload.

Also updates the MCP edit-tool refresh note, README, and chat system
prompt wording to reflect that config edits now apply live instead of
requiring a manual RELOAD/view_reload call.
Classic missions already live-apply layer frames natively via essence.js,
so reloading on a config-mutation broadcast there is redundant and
destructive to unsaved local UI state. Extract the mode check already used
by buildToolAdapter into isModernMission() and require it before scheduling
the debounced reload.
Previously ingesting under a name that already existed silently replaced
all its features. Check geodatasetEntries() first and return a
needsConfirmation preview (with the feature count that would be lost)
unless confirm: true was passed; brand-new names still ingest in one call.
chat/README.md's E2E checklist said 14 tools; the MCP server actually
exposes 28 (admin 10, edit 5, view 6, catalog 3, dashboard 4). Document two
more pre-existing security caveats in mcp/README.md: any relay peer can
forge a config-mutation frame to force-reload modern-mode sessions of any
mission (same unauthenticated-relay family as the existing view-command
caveat), and /api/accounts/entries and /api/accounts/update are reachable
by any valid long-term token, not just admin ones.
- edit.ts: make LIVE_NOTE/RELOAD_NOTE mode-honest (classic applies live vs.
  shows RELOAD; modern auto-reloads via AgentBridge; view_reload is a
  manual fallback either way), matching the AgentBridge modern-only gate.
- edit.ts layer_add: error early with a clear hint when layer.name is
  missing instead of silently inserting an unnamed layer entry; add a test.
- server.js: return after failureCallback() in validateLongTermToken's
  inner catch so a malformed query result can't also fall through and
  double-invoke a callback.
- agentLoop.js SYSTEM_PROMPT: clarify that provided config JSON for an
  EXISTING mission should go through mission_update_config as a merge
  patch, reserving dashboard_create_from_config for new missions.
- stac.ts: follow rel=next links in searchCollections (bounded at 10
  pages) so paginated STAC catalogs like VEDA don't silently drop
  collections before keyword filtering.
- Standardize the mission-name tool arg to `mission` across edit.ts,
  admin.ts (mission_delete), and dashboard.ts (schema arg only; maps
  to DashboardSpec.missionName internally).
- Add a `hint` telling the model to retry with confirm: true on every
  needsConfirmation result (mission_delete, geodataset_delete,
  geodataset_ingest overwrite, user_create, user_set_permission).
- mission_delete now pre-checks the mission exists via listMissions
  before preview or destroy, since the backend returns success for a
  nonexistent mission.
- view.ts: clarify the mission arg must be the exact mission_list name
  (case and spaces matter).
Add tools/result.ts's wrap() helper and route every admin/catalog/dashboard/
edit tool handler through it instead of a per-handler try/catch, and factor
admin.ts's five confirm-preview payloads through a shared confirmationNeeded()
helper. geodataset_ingest now only re-fetches the geodataset list when a
preview is actually needed (confirm !== true).
Add API/Backend/Utils/permissions.js's isSuperAdminRequest() and use it in
configs.js's /add guard and both signup-gate clauses in users.js (behavior
unchanged; the users.js clauses are the negation of the same check). Also
factor scripts/server.js's repeated long-term-token -> req field mapping
(ensureAdmin, ensureUser, annotateLongTermToken) into one
decorateReqFromTokenData() helper.
- chat/lib/agentLoop.js: drop the redundant parsed = null reassignment in
  the JSON.parse catch block.
- chat/public/app.js: add an appendAndScroll() helper and use it in
  addBubble, addToolCard, and the streaming-text scroll update.
- AgentBridge.js: factor the repeated ok ? {ok:true,...} : {ok:false,...}
  shape in the modern openTool branches into a small toOpenResult() helper.
@github-actions

Copy link
Copy Markdown

🤖 Version Auto-Bumped

The version has been automatically incremented to 4.2.19-20260728

This commit was added to your PR branch. When you merge this PR, the new version will be included.


If you want a different version, update package.json manually and push to this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant