Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
994057f
Add agentic MMGIS umbrella design spec
ajinkyakulkarni Jul 23, 2026
e8db3c8
Add Phase 1 implementation plan for agentic MMGIS
ajinkyakulkarni Jul 23, 2026
ab23714
Scaffold MMGIS MCP server package with env config
ajinkyakulkarni Jul 23, 2026
a801922
Normalize wsUrl to always end with exactly one trailing slash
ajinkyakulkarni Jul 23, 2026
f995d06
Add MMGIS REST client with long-term token auth
ajinkyakulkarni Jul 23, 2026
b5f1cf2
Honor long-term tokens in /api/configure/add SuperAdmin check
ajinkyakulkarni Jul 23, 2026
00651e1
Add MCP server skeleton with mission admin tools
ajinkyakulkarni Jul 23, 2026
9c4327d
Add dashboard profile builder wrapping the mission config generator
ajinkyakulkarni Jul 23, 2026
f205a51
Add dashboard generation MCP tools
ajinkyakulkarni Jul 23, 2026
6f4238f
Add STAC catalog search and layer conversion tools
ajinkyakulkarni Jul 23, 2026
bef7af5
Fix STAC non-OK hint and query string encoding
ajinkyakulkarni Jul 23, 2026
3770c6f
Add AgentBridge plugin component for browser-side agent control
ajinkyakulkarni Jul 23, 2026
207d172
[109] Guard AgentBridge websocket path against static builds and miss…
ajinkyakulkarni Jul 23, 2026
2e3f523
Add websocket bridge client and browser view control tools
ajinkyakulkarni Jul 23, 2026
7373d00
Fix BridgeClient.connect() to dedupe concurrent connection attempts
ajinkyakulkarni Jul 23, 2026
44d5664
Register MMGIS MCP server and add setup/demo documentation
ajinkyakulkarni Jul 23, 2026
0e56ec6
Remove .mcp.json from .gitignore
ajinkyakulkarni Jul 23, 2026
400806a
[final-review] Fix open_tool false success in modern-mode missions
ajinkyakulkarni Jul 23, 2026
7d50de0
[final-review] Preflight mission names and warn on missing MAPBOX_TOKEN
ajinkyakulkarni Jul 23, 2026
d3349ec
[final-review] Wrap res.json() with a diagnosable MMGISError
ajinkyakulkarni Jul 23, 2026
fbc61be
[final-review] Validate STAC_CATALOGS shape after parsing
ajinkyakulkarni Jul 23, 2026
5575555
[final-review] Make BridgeClient survive post-open socket errors
ajinkyakulkarni Jul 23, 2026
81fce7c
[final-review] Correct README security notes and E2E checklist
ajinkyakulkarni Jul 23, 2026
f9b7558
Fix AgentBridge open_tool id resolution and guard bridge reconnect state
ajinkyakulkarni Jul 23, 2026
42dfc0e
Flatten AgentBridge config.json to the shape updateComponents expects
ajinkyakulkarni Jul 23, 2026
cbe1cf0
Add chat UI design spec
ajinkyakulkarni Jul 25, 2026
715ffac
Add JSON config round-trip to chat UI spec
ajinkyakulkarni Jul 25, 2026
89de14e
Add chat UI implementation plan
ajinkyakulkarni Jul 25, 2026
4d2e047
Add dashboard JSON config round-trip to MCP server
ajinkyakulkarni Jul 25, 2026
a8c40d5
Scaffold standalone chat app with env config
ajinkyakulkarni Jul 25, 2026
cc53828
Add MCP bridge exposing tools as OpenAI function schemas
ajinkyakulkarni Jul 25, 2026
a095325
Add OpenAI function-calling agent loop
ajinkyakulkarni Jul 25, 2026
abff147
Add chat server with SSE streaming endpoints
ajinkyakulkarni Jul 25, 2026
7904710
Add test for GET /api/health graceful degradation when MCP bridge is …
ajinkyakulkarni Jul 25, 2026
77fb4b5
Add chat frontend with tool cards and JSON config drawer
ajinkyakulkarni Jul 25, 2026
305ac14
Fix chat frontend review findings: bubble dup, races, XSS, robustness
ajinkyakulkarni Jul 25, 2026
39a29e1
Add chat app documentation and register it in the project structure
ajinkyakulkarni Jul 25, 2026
1b94c05
[chat] Harden MCP bridge lifecycle and error serialization for merge
ajinkyakulkarni Jul 25, 2026
61aeb91
Add chat-driven full configuration design spec
ajinkyakulkarni Jul 25, 2026
1a2ca8c
Add chat-driven full configuration implementation plan
ajinkyakulkarni Jul 25, 2026
a2803b5
Add REST client methods for config editing and admin operations
ajinkyakulkarni Jul 25, 2026
352469a
Add RFC 7386 merge patch and config edit helper
ajinkyakulkarni Jul 25, 2026
f32f3db
Add live config editing tools
ajinkyakulkarni Jul 25, 2026
af3d10c
Add mission and geodataset admin tools with confirmation gating
ajinkyakulkarni Jul 25, 2026
4fcb897
Add user admin tools and honor SuperAdmin tokens in signup
ajinkyakulkarni Jul 25, 2026
3a4db10
Add catch handler to validateLongTermToken promise chain to prevent h…
ajinkyakulkarni Jul 25, 2026
73eac40
Add view_reload bridge command for applying non-layer config changes
ajinkyakulkarni Jul 25, 2026
c30c476
Teach the chat agent config editing and admin workflows
ajinkyakulkarni Jul 25, 2026
8d2fc54
Auto-reload modern-mode sessions via AgentBridge on config mutations
ajinkyakulkarni Jul 25, 2026
0949552
Gate AgentBridge config-mutation auto-reload to modern-mode sessions
ajinkyakulkarni Jul 25, 2026
f310ed9
Require confirm before geodataset_ingest overwrites an existing dataset
ajinkyakulkarni Jul 25, 2026
d779c9f
Fix tool-count and security-note gaps in READMEs
ajinkyakulkarni Jul 25, 2026
9f15729
Fold in remaining final-review fixes
ajinkyakulkarni Jul 25, 2026
33ef68a
[MCP] Fix five live-LLM-test findings in the MCP server
ajinkyakulkarni Jul 26, 2026
eb1a5f1
Raise STAC collection pagination cap to cover large catalogs
ajinkyakulkarni Jul 26, 2026
3507e13
Show thinking indicator and add request timeout in chat UI
ajinkyakulkarni Jul 26, 2026
2e21c02
Guide catalog searches past thematic keywords with synonyms and samples
ajinkyakulkarni Jul 26, 2026
dc976da
Fall back to MapLibre basemap when MAPBOX_TOKEN is unset
ajinkyakulkarni Jul 26, 2026
1621678
Route catalog items to their own tile servers and drop the scale suffix
ajinkyakulkarni Jul 26, 2026
5ffae4b
Build item-path tile URLs for titiler-pgstac catalogs like VEDA
ajinkyakulkarni Jul 26, 2026
3b8228d
Add split-panel layout with embedded live dashboard
ajinkyakulkarni Jul 26, 2026
822f016
Route the dashboard panel through the UI origin to keep mission queries
ajinkyakulkarni Jul 26, 2026
3b95f7a
List every collection id on empty catalog matches and distrust stale …
ajinkyakulkarni Jul 26, 2026
15c4273
Restyle chat UI with a light professional theme and reset stale panel…
ajinkyakulkarni Jul 26, 2026
edafd5f
Present dataset choices with coverage before building thematic dashbo…
ajinkyakulkarni Jul 26, 2026
585d86a
[mcp] Dedupe tool-handler error wrapping and confirmation previews
ajinkyakulkarni Jul 27, 2026
8639ec0
[backend] Extract shared SuperAdmin check and token-decoration helper
ajinkyakulkarni Jul 27, 2026
968f106
[chat, frontend] Small dead-code and duplication cleanups
ajinkyakulkarni Jul 27, 2026
58d41f9
Match mission names leniently in the agent bridge
ajinkyakulkarni Jul 27, 2026
de64936
chore: bump version to 4.2.19-20260728 [version bump]
github-actions[bot] Jul 28, 2026
c0d7641
Explain MMGIS_URL and MMGIS_TOKEN in the setup docs
ajinkyakulkarni Jul 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ nul
# Wildcard patterns for component plugins
/src/essence/*Private-Components*
/src/essence/*Plugin-Components*
!/src/essence/MMGIS-Plugin-Components/

/src/pre/tools.js
/src/pre/components.js
Expand All @@ -54,8 +55,6 @@ sessions
.terraform/
.terraform.lock.hcl


.mcp.json
.serena
.claude/*
!.claude/skills/
Expand Down
12 changes: 12 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"mcpServers": {
"mmgis": {
"command": "node",
"args": ["mcp/dist/index.js"],
"env": {
"MMGIS_URL": "http://localhost:8888",
"MMGIS_TOKEN": "${MMGIS_TOKEN}"
}
}
}
}
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ MMGIS/
│ └── Ancillary/ # UI components and helpers
├── configure/ # Admin configuration interface
│ └── build/ # Configuration UI
├── mcp/ # MCP server: agents drive MMGIS + generate dashboards
├── chat/ # Standalone chat UI: OpenAI-driven MMGIS control via the MCP server
├── docs/ # Documentation (Jekyll site)
├── public/ # Static assets
├── Missions/ # Mission data storage
Expand Down
4 changes: 3 additions & 1 deletion API/Backend/Config/routes/configs.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const Config = require("../models/config");
const config_template = require("../../../templates/config_template");
const userModel = require("../../Users/models/user");
const User = userModel.User;
const { isSuperAdminRequest } = require("../../Utils/permissions");

// Sanitize user input to prevent XSS in error messages
function sanitizeInput(input) {
Expand Down Expand Up @@ -381,7 +382,8 @@ function add(req, res, next, cb) {

if (fullAccess)
router.post("/add", function (req, res, next) {
if (req.session.permission !== "111") {
const isSuperAdmin = isSuperAdminRequest(req);
if (!isSuperAdmin) {
res.send({
status: "failure",
message: "Only SuperAdmins can add new missions.",
Expand Down
5 changes: 3 additions & 2 deletions API/Backend/Users/routes/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const buf = crypto.randomBytes(128);
const logger = require("../../../logger");
const userModel = require("../models/user");
const User = userModel.User;
const { isSuperAdminRequest } = require("../../Utils/permissions");

function isStrongPassword(password) {
const minLength = 8;
Expand Down Expand Up @@ -81,12 +82,12 @@ router.post("/first_signup", function (req, res, next) {
router.post("/signup", function (req, res, next) {
if (
(process.env.AUTH === "local" &&
req.session.permission !== "111" &&
!isSuperAdminRequest(req) &&
!(
process.env.AUTH_LOCAL_ALLOW_SIGNUP === true ||
process.env.AUTH_LOCAL_ALLOW_SIGNUP === "true"
)) ||
(process.env.AUTH === "off" && req.session.permission !== "111")
(process.env.AUTH === "off" && !isSuperAdminRequest(req))
) {
res.send({
status: "failure",
Expand Down
7 changes: 6 additions & 1 deletion API/Backend/Users/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ const userModel = require("./models/user");
let setup = {
//Once the app initializes
onceInit: (s) => {
s.app.use(s.ROOT_PATH + "/api/users", s.checkHeadersCodeInjection, router);
s.app.use(
s.ROOT_PATH + "/api/users",
s.checkHeadersCodeInjection,
s.annotateLongTermToken,
router
);
},
//Once the server starts
onceStarted: (s) => {},
Expand Down
7 changes: 7 additions & 0 deletions API/Backend/Utils/permissions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
function isSuperAdminRequest(req) {
return (
(req.session && req.session.permission === "111") ||
(req.isLongTermToken === true && req.tokenUserPermission === "111")
);
}
module.exports = { isSuperAdminRequest };
22 changes: 22 additions & 0 deletions chat/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Your OpenAI API key (required). Never sent to the browser.
OPENAI_API_KEY=sk-...
OPENAI_MODEL=gpt-4o
CHAT_PORT=8895

# How to launch the MMGIS MCP server (relative paths resolve against chat/)
MCP_COMMAND=node
MCP_ARGS=../mcp/dist/index.js

# Passed through to the MCP server process.
# MMGIS_URL is YOUR running MMGIS instance (whatever PORT its .env uses; 8888 by default).
# MMGIS_TOKEN is an MMGIS API token you generate yourself: log into MMGIS as an
# admin, open the Configure page, and use the "API Tokens" tab. It is not a
# shared secret — each person mints their own, and it inherits that account's
# permissions (creating missions needs a SuperAdmin token).
MMGIS_URL=http://localhost:8888
MMGIS_TOKEN=
MAPBOX_TOKEN=

# Where the dashboard UI is served, if different from MMGIS_URL
# (in dev, webpack serves the UI on PORT+1 and its redirect drops query params)
MMGIS_DASHBOARD_URL=http://localhost:8889
2 changes: 2 additions & 0 deletions chat/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
.env
73 changes: 73 additions & 0 deletions chat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# MMGIS Chat UI

A standalone chat app for driving MMGIS with your own OpenAI key: describe a
dashboard, watch the MCP tools fire, open the result, and steer the live map —
all from a browser chat.

## Quickstart

1. Build the MCP server once: `cd ../mcp && npm install && npm run build`
2. `cd chat && npm install`
3. `cp .env.example .env` and set:
- `OPENAI_API_KEY` — your own OpenAI key (stays server-side, never sent to the browser)
- `MMGIS_URL` — your running MMGIS instance, e.g. `http://localhost:8888`
- `MMGIS_TOKEN` — an MMGIS API token you generate in MMGIS's **Configure →
API Tokens** tab (each person mints their own; it inherits that account's
permissions, and creating missions needs a SuperAdmin token)
4. `npm start` → open http://localhost:8895

## What you can do

- "Create an air quality dashboard over Atlanta" → watch `catalog_*` +
`dashboard_generate` fire; click "Open dashboard →".
- "Show me the config JSON for that dashboard" → the model calls
`dashboard_generate` with `returnConfig: true`; copy the JSON from the tool card.
- **JSON config** drawer → paste/edit config JSON, name it, "Create dashboard
from JSON" (runs `dashboard_create_from_config` through the agent, visibly).
- With a dashboard open in another tab: "fly the map to Huntsville" (`view_*`
tools drive that session over the MMGIS websocket).
- "Make the OSM layer 50% transparent", "rename the page to Flood Watch then reload the view", "upload this GeoJSON and add it as a layer", "delete the JSON Demo mission" → live config editing and confirmation workflows.

## How it works

Browser (static page, SSE) → `server.js` (Express; your key stays here) →
OpenAI function calling → MCP client over stdio → `../mcp/dist/index.js` →
MMGIS REST + websocket. Conversation state lives in your browser
(localStorage); the server is stateless.

## Env vars

| Var | Default | Purpose |
| --- | --- | --- |
| `OPENAI_API_KEY` | (required) | Server-side only |
| `OPENAI_MODEL` | `gpt-4o` | Chat model |
| `CHAT_PORT` | `8895` | UI port |
| `MCP_COMMAND` / `MCP_ARGS` | `node` / `../mcp/dist/index.js` | MCP server launch (paths relative to `chat/`) |
| `MMGIS_URL` | `http://localhost:8888` | Your running MMGIS instance |
| `MMGIS_TOKEN` | — | MMGIS API token (Configure → API Tokens) |
| `MAPBOX_TOKEN`, `STAC_CATALOGS`, ... | — | Passed through to the MCP server |

## Known limitations

- Cross-turn memory only replays user/assistant text — tool results are not
persisted between turns. If you need the model to recall a config from
earlier in the conversation, ask it to echo the config back rather than
relying on it to remember the raw tool output.
- Very large configs may not round-trip faithfully through the JSON drawer
(the model can hit output length limits when repeating them back).

## Manual E2E checklist

- [ ] `/api/health` shows the model and `MCP connected` with 28 tools
- [ ] Simple prompt streams a text reply
- [ ] Dashboard request shows tool cards and an "Open dashboard →" button that loads in MMGIS
- [ ] "show me the config JSON" returns the full config in a tool card
- [ ] JSON drawer creates a mission from pasted (edited) config
- [ ] `view_fly_to` request visibly moves an open dashboard's map
- [ ] Bad OpenAI key shows a red error bubble, conversation survives a retry
- [ ] layer_update from chat visibly changes an open dashboard WITHOUT reloading (e.g. opacity)
- [ ] mission_update_config + view_reload applies a basemap/page-name change
- [ ] geodataset_ingest (inline) → layer_add with geodatasets:<name> renders the data
- [ ] mission_delete asks for confirmation in chat before acting
- [ ] user_create + user_set_permission work with the long-term token (exercises the flagged backend change)
- [ ] mission_clone (may fail if the MMGIS host lacks a `python` binary — record outcome)
85 changes: 85 additions & 0 deletions chat/lib/agentLoop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
export const SYSTEM_PROMPT = `You are an assistant that builds and drives MMGIS dashboards through tools.

Workflow guidance:
- Before generating a dashboard, call dashboard_profile_schema (input shape + layer examples) and dashboard_tool_options (valid tool names).
- Find data layers with catalog_collections / catalog_search, and convert items with catalog_item_to_layer.
- Catalog dataset names are technical, not thematic: for "air quality" search no2, so2, pm, aerosol; for "wildfire" search fire, burn, thermal. Try 2-3 synonyms and scan the availableCollections list before concluding no data exists.
- When a request is thematic ("flooding", "fires") and several distinct datasets match, DO NOT silently pick one. Present a short numbered list of the best candidates — plain-language title, whether it is event-specific (small area, days) or ongoing/global (use temporalExtent/spatialExtent), and dates — then ask the user to choose before building. Skip asking only when the request is already specific or exactly one reasonable match exists. Always state in your final summary exactly which datasets you used.
- Mission names must avoid punctuation (letters, numbers, spaces, underscores are safe).
- After dashboard_generate or dashboard_create_from_config succeeds, ALWAYS give the user the mission URL.
- When the user wants to see or edit the raw config, call dashboard_generate with returnConfig: true and show the JSON.
- When the user provides config JSON, install it with dashboard_create_from_config. For an EXISTING mission, apply provided JSON as a mission_update_config patch instead (merge semantics: absent keys survive, null deletes); dashboard_create_from_config is for new missions.
- Use view_* tools to drive a browser session that has the mission open (view_get_state first if unsure).
- Tool errors include a "hint" — follow it to self-correct. If you cannot recover, tell the user the error and hint plainly.
- Editing existing dashboards: prefer layer_add/layer_update/layer_remove and tool_toggle. For anything else use mission_update_config with a JSON merge-patch (null deletes a key; arrays replace). Edits apply live — layer edits AND config patches — so there's no need to call view_reload after every edit; it remains available as a manual fallback if a session doesn't pick up the change.
- DESTRUCTIVE tools (mission_delete, geodataset_delete, user_create, user_set_permission) return needsConfirmation first. Show the user exactly what will happen, get their explicit yes, then retry with confirm: true. Never set confirm on your own.
- Geodata: ingest GeoJSON with geodataset_ingest (inline for small data, url for hosted files), then add a layer with type "vector" and url "geodatasets:<name>".
- User management: new users start as Viewer (001); promote with user_set_permission (110 Admin / 001 Viewer; SuperAdmin cannot be granted). Never repeat passwords back.
- If earlier messages in this conversation claimed data was unavailable, do not trust that claim — catalogs and tools improve; re-search fresh each time the user asks.
- Be concise. Never invent tool results.`

export async function runAgentLoop({ messages, openai, bridge, model, onEvent, maxIterations = 15 }) {
const tools = await bridge.getOpenAiTools()
const convo = [{ role: 'system', content: SYSTEM_PROMPT }, ...messages]

for (let i = 0; i < maxIterations; i++) {
const { text, toolCalls } = await streamOneTurn({ openai, model, messages: convo, tools, onEvent })
if (toolCalls.length === 0) {
onEvent({ type: 'done' })
return
}
convo.push({
role: 'assistant',
content: text || null,
tool_calls: toolCalls.map((c) => ({
id: c.id,
type: 'function',
function: { name: c.name, arguments: c.args },
})),
})
for (const c of toolCalls) {
let parsed = null
try {
parsed = c.args ? JSON.parse(c.args) : {}
} catch {}
onEvent({ type: 'tool_call', id: c.id, name: c.name, args: parsed ?? {} })
const result =
parsed === null
? { text: JSON.stringify({ error: `Invalid JSON arguments: ${c.args}` }), isError: true }
: await bridge.callTool(c.name, parsed)
onEvent({ type: 'tool_result', id: c.id, name: c.name, result: result.text, isError: result.isError })
convo.push({ role: 'tool', tool_call_id: c.id, content: result.text })
}
}

// Loop guard: force a final, tool-free summary
convo.push({ role: 'user', content: 'Tool budget exhausted — summarize what you did and stop.' })
await streamOneTurn({ openai, model, messages: convo, onEvent })
onEvent({ type: 'done' })
}

async function streamOneTurn({ openai, model, messages, tools, onEvent }) {
const stream = await openai.chat.completions.create({
model,
messages,
stream: true,
...(tools ? { tools } : {}),
})
let text = ''
const toolCalls = []
for await (const chunk of stream) {
const delta = chunk.choices?.[0]?.delta
if (!delta) continue
if (delta.content) {
text += delta.content
onEvent({ type: 'text', delta: delta.content })
}
for (const tc of delta.tool_calls || []) {
const slot = (toolCalls[tc.index] ??= { id: '', name: '', args: '' })
if (tc.id) slot.id = tc.id
if (tc.function?.name) slot.name += tc.function.name
if (tc.function?.arguments) slot.args += tc.function.arguments
}
}
return { text, toolCalls: toolCalls.filter(Boolean) }
}
71 changes: 71 additions & 0 deletions chat/lib/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import express from 'express'
import { runAgentLoop } from './agentLoop.js'

const PUBLIC_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'public')

export function createApp({ cfg, openai, bridge }) {
const app = express()
app.use(express.json({ limit: '2mb' }))
app.use(express.static(PUBLIC_DIR))

app.get('/api/health', async (req, res) => {
let toolCount = 0
try {
toolCount = (await bridge.getOpenAiTools()).length
} catch {
// leave toolCount 0; mcpConnected reflects reality below
}
res.json({
ok: true,
model: cfg.model,
mcpConnected: bridge.isConnected(),
toolCount,
mmgisUrl: cfg.mcpEnv?.MMGIS_URL ?? null,
// Where the dashboard UI is served (webpack dev server differs from the API port)
dashboardUrl: cfg.mcpEnv?.MMGIS_DASHBOARD_URL ?? cfg.mcpEnv?.MMGIS_URL ?? null,
})
})

app.get('/api/missions', async (req, res) => {
try {
const out = await bridge.callTool('mission_list', {})
if (out.isError) return res.status(502).json({ error: out.text })
res.json({ missions: JSON.parse(out.text).missions ?? [] })
} catch (err) {
res.status(502).json({ error: String(err?.message ?? err) })
}
})

app.get('/api/tools', async (req, res) => {
try {
const tools = await bridge.getOpenAiTools()
res.json({ tools: tools.map((t) => ({ name: t.function.name, description: t.function.description })) })
} catch (err) {
res.status(502).json({ error: err.message })
}
})

app.post('/api/chat', async (req, res) => {
const { messages } = req.body || {}
const valid =
Array.isArray(messages) &&
messages.every((m) => m && typeof m.content === 'string' && ['user', 'assistant'].includes(m.role))
if (!valid) {
return res.status(400).json({ error: 'body must be {messages: [{role: user|assistant, content: string}]}' })
}
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Connection', 'keep-alive')
const send = (event) => res.write(`data: ${JSON.stringify(event)}\n\n`)
try {
await runAgentLoop({ messages, openai, bridge, model: cfg.model, onEvent: send })
} catch (err) {
send({ type: 'error', message: String(err?.message ?? err) })
}
res.end()
})

return app
}
18 changes: 18 additions & 0 deletions chat/lib/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'

export function loadConfig(env = process.env) {
if (!env.OPENAI_API_KEY) {
throw new Error('OPENAI_API_KEY is required — copy chat/.env.example to chat/.env and set it')
}
return {
apiKey: env.OPENAI_API_KEY,
model: env.OPENAI_MODEL || 'gpt-4o',
port: parseInt(env.CHAT_PORT || '8895', 10),
mcpCommand: env.MCP_COMMAND || 'node',
mcpArgs: (env.MCP_ARGS || '../mcp/dist/index.js').split(' ').filter(Boolean),
// chat/lib -> chat/
mcpCwd: path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'),
mcpEnv: { ...env },
}
}
Loading