Admin & Configuration Guide
For administrators. This page covers every configuration surface: the admin pages, the config files, the skills (the assistant's instructions), and the embeddings catalog and its command-line tools. admin role required for the management pages & APIs
On first run, Strata seeds a single administrator:
admin / strata (the default password). It is seeded
once and persisted to config/users.json — never re-applied on
later restarts. Change it after your first login. There is no in-app
change-password screen by design; rotate it from the command line:
./strata-geoai hash-password 'your-new-password'
Paste the printed Argon2id hash into the admin user's
pw_hash in config/users.json, then restart. The file stores hashes
only (never plaintext) and is git-ignored. Additional accounts are created
from the Users page — no file editing (see §3).
1. How it fits together
Three layers drive the assistant, and you tune each one differently:
- Data — REST only. Every data answer comes from the live ArcGIS Feature Service REST API against your layers. There is no local SQL engine and no data cache to manage.
- Instructions — the skills. Always-injected guidance that tells the Orchestrator and each specialist how to map plain language to tool calls. Edit these as Markdown files (§8).
- Retrieval — the embeddings. A local semantic catalog of your layers + API docs + tools. The assistant matches a question to the right layer/field/API by meaning. Managed from the Embeddings page (§9) and the CLIs (§10).
A request flows: [MAP CONTEXT] + question → Orchestrator plans → specialists (DataExploration,
Geoprocessing, MapControl, Charts, Help) run tools → reply.
2. Admin pages
These live in the slide-out drawer (☰) and are visible only to users whose role is admin
(set in config/users.json). The pages and their /geoai/admin/* APIs are enforced
server-side, so they stay protected even if linked directly.
The pages share one Settings tab bar, so once you're on any of them you can hop between the rest.
| Page | URL | What you manage |
|---|---|---|
| Layer Reference | /admin.html |
Register & curate the layers the assistant knows about: which fields it sees, field aliases, and per-layer AI instructions. Auto-creates a default entry when you add an unregistered URL. |
| Embeddings | /embeddings.html |
View / add / edit / delete every catalog entry (layer, doc, tool — all languages) and export the whole catalog as Markdown. See §9. |
| MCP Server | /mcp.html |
Register external MCP tool servers the agents can call, and get the Claude Desktop connector URL (§5, §11). |
| Basemaps | /basemaps.html |
Add / edit the basemap library (also editable in config.json → basemap.library). |
| Users | /users.html |
Add team members, set their role, change roles, remove accounts. See §3. |
| License | /license.html |
See the current plan (Free / Enterprise) and activate a license key. See the README. |
| Plugins | /plugins.html |
Manage optional feature plugins bundled with the deployment. |
| About | /about.html |
Build / version and deployment info. |
Admin role is granted in config/users.json (each account has a role — see §3).
The drawer links unhide for admins; the /geoai/admin/* endpoints return 403 for
everyone else, so the pages stay protected even if linked directly.
3. Authentication, users & roles
Access is controlled by one flag: server.require_login in config.json
(shipped default true). With it on, every data / chat / admin surface needs
an identity; with it off (local dev) everything is open and no sign-in is asked. The bundled web UI signs in
with a session cookie; external apps use OAuth tokens (§4) — both issued by
the same user store, so identity is unified.
Signing in
- Login page —
/login.html. A valid username + password sets anHttpOnlysession cookie (cng_session, 12-hour lifetime); Log out is in the drawer. - Each signed-in user gets their own isolated live map — one person's layers, selection, and saved-map edits never leak into another's session.
- Self-service sign-up (
/register.html) is disabled whenrequire_login=true; admins provision accounts on the Users page instead.
Roles
| Role | Can do |
|---|---|
admin | Everything — the management pages & /geoai/admin/* APIs
(users, OAuth apps, catalog), plus view/edit any saved map. |
author | Normal user — chat, query, and create / own / edit maps.
No management pages. (Legacy "user" records read as author.) |
viewer | Read-only — chat & explore, but cannot save/own maps. |
Saved-map access (ACL). Every saved map has an owner and a
visibility: private (owner only), shared (owner + named grants,
each view or edit), or public (everyone can view). Owners set this in the map
properties dialog; admins can view/edit anything. Legacy maps normalize to a public, admin-owned entry on
first read.
Managing users
Open Settings → Users (/users.html). You can add a member
(username, password, role), change a role, and remove an account. The store
refuses deleting yourself or the last remaining admin. Accounts persist to config/users.json;
passwords are Argon2id-hashed (never plaintext), the file is git-ignored.
pw_hash:
./strata-geoai hash-password 'the-new-password'
then restart. (New accounts get their password set for them on the Users page — no hashing by hand.)4. Signing in from an MCP client (OAuth 2.0)
Strata runs a small OAuth 2.0 authorization server for exactly one purpose: to let an MCP client — Claude Desktop — sign you in, so its tool calls run against your map. It is a sign-in mechanism, not an app platform.
client_credentials grant, so
nothing can obtain a token that acts on its own — every token belongs to a person who signed in and approved it.
(The old Settings → Apps & API page has been removed.)How it works
- The client discovers the server from
GET /.well-known/oauth-protected-resource(advertised by the401 WWW-Authenticatethe MCP endpoint returns) andGET /.well-known/oauth-authorization-server. - It registers itself as a public PKCE client at
POST /oauth/register(RFC 7591). No secret is issued. - It sends you to
GET /oauth/authorize. You sign in with your normal account, then meet a consent screen naming the client and the access it wants. Every client sees this screen — the registration endpoint is open by necessity, so your explicit approval is what makes a client trusted. - It exchanges the one-time code at
POST /oauth/token(PKCE S256 mandatory, code valid 60 s, single use) for an access token (1 hour) and a refresh token (30 days, rotated on use). - It sends
Authorization: Bearer <token>to/mcp. Calls run against your session (user:<name>) and are logged with it tologs/mcp.log.
Endpoints & scopes
| Purpose | Route |
|---|---|
| AS metadata / discovery | /.well-known/oauth-authorization-server |
| Protected-resource metadata | /.well-known/oauth-protected-resource |
| Client self-registration (public PKCE only) | POST /oauth/register |
| Authorize (code + PKCE, with consent) | GET /oauth/authorize |
| Token (code / refresh) | POST /oauth/token |
Scopes: maps:read, maps:write, mcp:tools
(space-separated; omit to get all three). Deleting a user immediately invalidates their tokens.
server.public_url in config.json (e.g.
https://stratageoai.localhost) so the issuer in the discovery document matches your TLS URL.First-party web UI vs. MCP clients
The bundled web UI signs in with a session cookie, not an OAuth token — the standard first-party pattern. The cookie and OAuth tokens come from the same user store, so identity is unified; OAuth is the door for MCP clients only (Claude Desktop — §5).
Errors
| Status | Meaning |
|---|---|
401 + WWW-Authenticate (on /mcp) |
No / invalid token — discover the AS from the header and sign in. |
400 invalid_client_metadata (on /oauth/register) |
The client asked for a secret or a grant this server doesn't issue. Public PKCE clients only. |
400 invalid_grant |
Code expired / already used / PKCE mismatch / redirect mismatch. |
403 (on /oauth/authorize) |
User not signed in (bounced to the login page). |
5. Connect Claude Desktop
Claude Desktop can drive this map directly: it connects to the MCP server as a
native custom connector and calls the map tools. Map-mutating tools (add a layer, render by a
field, buffer…) render on the live web map you have open at /; data/query tools return answers in
Desktop. It runs under the Desktop subscription — no API key.
Prerequisites — HTTPS
Custom connectors require an https:// URL. The app serves plain HTTP on
127.0.0.1:8767/mcp; the bundled Caddy front provides the TLS hostname
https://stratageoaimcp.localhost/mcp. So before connecting:
- the app is running (
./start.sh), - Caddy is up on
:443— it needssudo; ifhttps://…localhostcan't be reached buthttp://127.0.0.1:8766works, Caddy just isn't running (sudo caddy start --config ./Caddyfile), - the mkcert local CA is trusted (once:
mkcert -install).
See the MCP Server page (/mcp.html) for the copy-paste connector URL and the local-HTTPS setup.
Add the connector
- In Claude Desktop: Settings → Connectors → Add custom connector.
- Paste
https://stratageoaimcp.localhost/mcpand save, then fully quit (⌘Q) and reopen Claude Desktop. - Keep the map open at
/and prompt Desktop — e.g. “add the traffic cameras layer and render by county.”
Sign-in happens automatically over OAuth. With require_login on, Desktop's first
call to /mcp gets a 401 with a WWW-Authenticate header pointing at the
protected-resource metadata; Desktop discovers the authorization server, registers itself (DCR), runs the
auth-code sign-in against your Strata login, and then calls with the token. Each person's tools run against
their own map.
Fallback — the mcp-remote stdio bridge (OAuth-free)
If the native connector won't take the URL or OAuth misbehaves, bridge the plain-HTTP port via a local
process. ./start.sh --claude writes this into
~/Library/Application Support/Claude/claude_desktop_config.json for you (and backs up the old file):
{ "mcpServers": {
"strata-geoai": { "command": "npx",
"args": ["-y", "mcp-remote", "http://127.0.0.1:8767/mcp", "--allow-http"] } } }
This runs locally so it bypasses OAuth entirely. With require_login on it must still pass a token
(--header "Authorization: Bearer $TOKEN"); for pure-local dev, keep require_login=false
and no token is needed. After editing the config, ⌘Q and relaunch Desktop.
https:// URL loads). Note: Claude Desktop connects from this
machine so it can reach *.localhost; claude.ai in a browser cannot reach
localhost — for web use a public tunnel or the stdio bridge.6. Configuration files
All configuration is file-based. Edits to config.json take effect on the
next server start (skills can hot-reload separately — see §8).
| File | Controls |
|---|---|
config/config.json | The main config — server, UI, LLM providers & modes, embeddings (duckdb), agents, basemaps, geoprocessing, charts, history. See the breakdown below. |
config/services.json | The seed layer catalog — each entry's
name / title / description / url / geometry / tags, plus optional
title_ar / description_ar for a bilingual embedding row. Re-seed with
rebuild-embeddings. |
config/users.json | Accounts and roles (admin gating). |
config/rest_samples_mdsf311.json | The verified source for REST
“natural-language → tool call” exemplars. Add entries here, then run build-rest-chunks
+ ingest-docs (§10). |
docs/refs/*.json | Documentation chunks embedded for grounding —
sdk_chunks.json (ArcGIS JS SDK), and the REST files (rest_chunks.json,
rest_maryland.json, rest_examples.json, rest_sample2.json).
Ingested by ingest-docs. |
prompts/system.md + skills/ | The assistant's instructions (§8). |
Key config.json sections
| Section | What it sets |
|---|---|
server | host, port (default 8766),
mcp_port, debug. |
ui | Panel width, initial map center/zoom,
tool_call_iteration_limit. |
llm | default_mode (local/cloud),
modes (which provider each mode uses), system_prompt_path,
skills_dir, hot_reload_skills, orchestrator.max_plan_steps,
and providers (§7). |
duckdb | Embeddings store: registry_db_path
(./data/registry.duckdb) and embedding_dim (1024 for bge-m3). |
agents | Per-specialist enabled + max_iterations
(tool-call budget) for orchestrator, data_exploration, geoprocessing, map_control, charts, help. |
basemap | default + the library of basemaps. |
geoprocessing / charts / rest_proxy / nominatim |
Buffer limits, chart defaults, the proxy host allow-list, and geocoder settings. |
7. Models & modes
Users toggle Local ↔ Cloud from the chat ☰ menu. The mapping lives in
llm.modes: local → ollama (qwen2.5:7b-instruct), cloud → anthropic
(Claude). The embedding model bge-m3 is infrastructure — it stays local in both modes and
is never a user choice.
- Change a chat model — edit the provider's
modelunderllm.providers(e.g.ollama.model,anthropic.model). - Add / switch providers —
llm.providersholds anthropic, gemini, grok, ollama, ollama_qwen3. Point a mode at one viallm.modes. - API keys — never stored in config; each cloud provider reads an env var
(
api_key_env, e.g.ANTHROPIC_API_KEY). Cloud mode returns409if the key is missing. - Changing the embedding model or
embedding_dimforces a full re-embed (different vector space). Runrebuild-embeddings+ingest-docsafter.
Setting up Ollama (local models)
The local mode and all embeddings run on Ollama — a local model runner. Install it, start it, and pull the two models the app
expects. Ollama serves on http://localhost:11434 (matches
llm.providers.ollama.base_url).
# 1. install Ollama (macOS: `brew install ollama`, or download from ollama.com), then start it
# with the recommended performance flags (see note below):
OLLAMA_FLASH_ATTENTION=1 OLLAMA_KV_CACHE_TYPE=q8_0 ollama serve # (the desktop app starts the server automatically)
# 2. pull the two models the app uses:
ollama pull qwen2.5:7b-instruct # local chat / agent model
ollama pull bge-m3 # embedding model
# 3. verify:
ollama list # both models should appear
Performance flags (recommended).
OLLAMA_FLASH_ATTENTION=1 speeds up long-context prefill, and
OLLAMA_KV_CACHE_TYPE=q8_0 halves the memory of the 16K-token KV cache (it
requires flash attention) — together they free enough headroom to run a larger local model
(e.g. qwen2.5:14b) comfortably on a 16 GB machine, with quicker prefill on
every turn. Set them on the ollama serve process. With the macOS desktop app
(which auto-starts the server) set them once with
launchctl setenv OLLAMA_FLASH_ATTENTION 1 and
launchctl setenv OLLAMA_KV_CACHE_TYPE q8_0, then restart Ollama.
| Model | Role | When it's used |
|---|---|---|
qwen2.5:7b-instruct |
The local chat / agent model. Runs the Orchestrator and specialists — reads the question + map context, plans steps, and makes tool calls. Tool-calling capable. | Local mode only. (Cloud mode uses Claude instead.) Swappable via
llm.providers.ollama.model — no re-embed needed. |
bge-m3 |
The embedding model (1024-dim, multilingual incl. Arabic). Turns every layer, doc, tool — and every user question — into a vector so the catalog can match by meaning. | Both modes — embeddings are always local. Required for semantic search and agent grounding even when chatting with Cloud. Changing it forces a full re-embed. |
qwen3:8b) is pre-configured as the
ollama_qwen3 provider — pull it (ollama pull qwen3:8b) and point a mode at it
if you want to compare. Cloud mode needs no Ollama model beyond bge-m3.8. Skills — the assistant's instructions
“Skills” are Markdown instruction packs. They are always injected (not retrieved), so this is where you put durable, authoritative guidance. Two parts compose per turn:
prompts/system.md— the base system prompt, split into XML-tagged sections (<Shared>,<Orchestrator>,<DataExploration>,<Geoprocessing>,<MapControl>,<Charts>,<Help>).skills/<role>/SKILL.md— a detailed pack appended for that role.
| Skill pack | Role |
|---|---|
skills/orchestrator/SKILL.md | Plans a request into steps; resolves layer ids, geocodes, picks the right specialist. Worked planning examples. |
skills/data_exploration/SKILL.md | All read-only data questions — counts, filters, stats, group-by, distinct, top-N, histograms, spatial filters, related records. |
skills/geoprocessing/SKILL.md | Spatial transforms that produce a new layer — buffer, intersect, clip, dissolve, convex hull, union, difference. |
skills/map_control/SKILL.md | View navigation, add/remove layers, symbology, labels, popups, basemaps. |
skills/charts/SKILL.md | Renders a chart from a rows result (only when the user used chart words). |
skills/help/SKILL.md | Capability questions and disambiguation. |
How to edit a skill
- Open the relevant
skills/<role>/SKILL.mdin a text editor. - Write senior-to-junior, concrete guidance: name the tool, give exact arg shapes,
flag case-sensitive field names. Use placeholders (
<layer_id>) and mark example field names as illustrative — the model fills real values from[MAP CONTEXT]. - Keep claims accurate: only reference tools/parameters that exist. A wrong parameter teaches the model to emit a broken call.
- Apply: set
llm.hot_reload_skills: trueinconfig.jsonto pick up edits live, or restart the server (the default).
9. Embeddings catalog
One local DuckDB table (./data/registry.duckdb) holds a semantic fingerprint of every
layer, doc chunk, and tool. Each row carries:
- embed_text — the string the vector is computed from (tuned for retrieval).
- grounding_text — the string injected into the prompt (tuned for readability — placeholdered URLs, “fields are illustrative” notes).
- agent — which specialist(s) a doc/tool serves (so grounding routes by agent).
- lang —
en/ar; bilingual layers/exemplars store one row per language (bge-m3 also bridges across languages).
Day-to-day curation happens on the Embeddings page (/embeddings.html):
view, add, edit, delete entries, and export the whole catalog as Markdown
(GET /geoai/admin/embeddings.md) for offline review. Most entries are created automatically when
you register a layer or run the doc ingest; you add entries by hand to teach extra domain knowledge.
10. Catalog command-line tools
Run from the project root with the server stopped. (Dev form shown; the built binary is
strata <subcommand>.) The local embedding service (Ollama + bge-m3) must be
running for any command that embeds.
| Command | What it does |
|---|---|
cargo run -- rebuild-embeddings | Re-seed all layer rows from
config/services.json (incl. bilingual ar rows). Run after editing layers. |
cargo run -- build-rest-chunks | Regenerate the REST doc chunk JSON in
docs/refs/ from the verified sources. Pure file transform (no embedding). |
cargo run -- ingest-docs | Embed the doc chunks from docs/refs/*.json
(SDK + REST). Run after build-rest-chunks or editing a doc file. |
cargo run -- catalog-lint | Validate the catalog — flags empty text, missing vectors, model mismatch, untagged docs, leaked URLs, and reports bilingual gaps / duplicates. Run before/after a rebuild. |
cargo run -- eval-retrieval | Score retrieval quality (recall@k / MRR) against
docs/eval_retrieval.json. |
cargo run -- eval-routing | Score how well the model classifies a request to the right REST operation (grounding off vs on). |
The routine “fine-tune” loop
“Fine-tuning” here means curating the catalog content (not retraining a model):
# 1. edit a source: config/services.json or config/rest_samples_mdsf311.json
# 2. stop the server, then (Ollama running):
cargo run -- build-rest-chunks # only if you changed REST exemplars
cargo run -- rebuild-embeddings # only if you changed layers
cargo run -- ingest-docs # embed docs
cargo run -- catalog-lint # expect 0 errors / 0 warnings
cargo run -- eval-retrieval # confirm recall held
11. MCP servers & basemaps
- MCP servers — register external Model Context Protocol tool servers on
/mcp.html(or via/geoai/admin/mcp-servers). Each adds tools the agents can call. - Basemaps — manage the gallery on
/basemaps.html, or editconfig.json → basemap.library(supportsxyzandtile_serversources).basemap.defaultsets the startup basemap.
12. Common tasks (cheat-sheet)
| I want to… | Do this |
|---|---|
| Make the assistant aware of a new layer | Add it on Layer Reference, or add
it to config/services.json and run rebuild-embeddings. |
| Improve how a layer is found / queried | On Layer Reference: trim fields, set aliases, write per-layer AI instructions. |
| Change how a specialist behaves | Edit its skills/<role>/SKILL.md (§8). |
| Add Arabic layer names | Set title_ar/description_ar in
services.json, then rebuild-embeddings. |
| Add a worked REST example | Add to config/rest_samples_mdsf311.json, then
build-rest-chunks + ingest-docs. |
| Switch the default model / mode | Edit llm.default_mode /
llm.providers.<p>.model in config.json; restart. |
| Check the catalog is healthy | cargo run -- catalog-lint (server down). |
| Review everything that's embedded | Export Markdown from the Embeddings
page, or GET /geoai/admin/embeddings.md. |
| Add a team member | Settings → Users → Add a user (username, password, role). §3. |
| Rotate the admin password | ./strata-geoai hash-password '…' → paste into
pw_hash in config/users.json; restart. §3. |
| Let Claude Desktop drive your map | Add it as a custom connector; it registers itself and you
sign in with your normal account. §4–§5 / docs/OAUTH_INTEGRATION.md. |
| Connect Claude Desktop | Add a custom connector for
https://stratageoaimcp.localhost/mcp (Caddy must be up). §5. |
| Turn login on / off | Set server.require_login in config.json
(or ./start.sh --login / --no-login); restart. §3. |