This MCP server enables LLM frontends (any MCP-capable client, custom agents) to interact with any GeoReport v2/Open311 compliant backend using the Model Context Protocol (MCP).
A stdio-only MCP client cannot talk to a remote HTTPS server directly. The
mcp-remote adapter bridges the gap:
the client launches it locally, and it forwards MCP traffic to the deployed server
over HTTPS, attaching your Bearer token.
Add the server to your MCP client's configuration file (the exact path depends on the client; for stdio clients it is typically a JSON config under the client's application support directory):
{
"mcpServers": {
"georeport-demo": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://<your-server-host>/api/mcp",
"--header",
"Authorization:${AUTH_HEADER}"
],
"env": {
"AUTH_HEADER": "Bearer <YOUR_BEARER_TOKEN>"
}
}
}
}Notes:
- The token lives in
env, not inline in--header, to dodgemcp-remote's space-in-argument quirk and keep the secret out of the args list. - The token is the access key: no token, no access (gate-everything). It also pins which jurisdictions and toolsets the session may use, so knowing the URL alone grants nothing.
- Needs Node (for
npx); the first launch downloadsmcp-remote. - To run it purely locally, start it yourself (
pnpm run build && node dist/server.js) and point the same config athttp://127.0.0.1:3100/api/mcpinstead.
Then fully quit and reopen your MCP client (most clients load MCP servers only at startup). In a new chat the server's tools should appear; try "List the service categories for jurisdiction 1."
Live demo endpoint: https://mcp.demo.mark-a-spot.com/api/mcp
(EU/DSGVO, TLS via Traefik+Let's Encrypt; requires a valid Bearer token)
MCP client / agent
|
mcp-remote (stdio <-> HTTPS adapter)
|
POST /api/mcp (JSON-RPC, stateless)
|
src/server.ts (Express, production entrypoint)
|
src/mcpApp.ts (shared handleMcp() + Bearer extraction)
|
GeoReport v2 backend (Open311)
Transport: stateless JSON-RPC over HTTP POST /api/mcp. No SSE, no sessions.
The dev server (src/index.ts, pnpm run dev) uses Express+SSE and is not for production.
The server ships 12 tools. Which are visible to a client depends on the toolset configuration for that deployment:
| Tool | Toolset | Description |
|---|---|---|
list_services |
read | Available report categories |
list_requests |
read | Service requests with filters: status, service_code, date range, location + radius |
get_request |
read | Single request details by ID |
search_location |
read | Geocode addresses to coordinates via Nominatim |
create_request |
intake | Create new service request (service_code, lat, long, description, optional media_url) |
prepare_upload |
intake | Generate a one-time browser upload URL and token |
upload_image |
intake | Direct base64 image upload |
check_upload |
intake | Poll upload status by token |
update_request |
manage | Change status, add notes, assign agency |
add_comment |
manage | Add comment without changing status |
list_tenants |
manage | Show configured tenants |
get_stats |
reporting | Aggregated KPI statistics (read-only) |
Toolsets (ENABLED_TOOLSETS, comma-separated) are cut by function, not persona:
read(baseline): browse tools (list_services,list_requests,get_request,search_location)intake(baseline): file reports and attach photos (create_request+ upload helpers); covers citizen self-service and staff on-behalfmanage(opt-in): staff write/admin (update_request,add_comment,list_tenants); requiresrole: staffreporting(opt-in): staff KPI analytics (get_stats); requiresrole: staff
read + intake are the default baseline for any valid token. Legacy persona aliases (citizen, staff) still resolve for backward compatibility.
The demo deployment enables read,intake,reporting and exposes 9 tools (no write tools, no list_tenants).
Default-deny. No token means no access, including read-only tools.
MCP_AUTH_USERS is a JSON array mapping Bearer tokens to scoped permissions:
[
{
"mcpToken": "<REPLACE_RANDOM_TOKEN>",
"name": "Display Name",
"tenant": "demo",
"role": "staff",
"allowedJurisdictions": ["1", "8"],
"allowedToolsets": ["read", "intake", "reporting"]
}
]Jurisdiction pinning is the primary isolation mechanism. A token with
allowedJurisdictions: ["1", "8"] cannot reach jurisdiction 5, even if the caller
sends jurisdiction_id: 5 explicitly. The dispatch rejects unknown jurisdictions; it
never silently overwrites them.
Toolset scoping (allowedToolsets) further restricts which tools the token
may call, independent of what the deployment enables globally.
Legacy single token (MCP_AUTH_TOKEN): unscoped operator access, staff role, no
jurisdiction pin. Only safe on a single-tenant server; rejected automatically on
multi-tenant deployments.
Generate tokens with: openssl rand -hex 32
Standard Open311 requires an external media_url. The server provides a browser-assisted
upload flow so agents can attach photos without clipboard access:
- Agent calls
prepare_uploadand receives an upload URL + token. - Agent presents the URL to the user. User opens it in a browser, previews the photo, and confirms.
- Agent polls
check_uploaduntilstatus: ready. - Agent uses the returned
media_urlincreate_request.
Photos are stored on the local filesystem (UPLOAD_DIR=/data/uploads, Docker volume).
TTL is 10 minutes. The GeoReport backend downloads media_url into its own storage at
create_request time; the server copy is dispensable afterwards.
The server targets one backend stack (one or more jurisdictions on one GeoReport v2 instance). It is not a central multiplexer across unrelated backends.
Single-tenant:
GEOREPORT_SERVER_URL=https://your-georeport-server.com
GEOREPORT_API_KEY=citizen_key
GEOREPORT_STAFF_API_KEY=staff_keyMulti-tenant (TENANTS_CONFIG overrides single-tenant vars):
TENANTS_CONFIG='[
{
"id": "demo",
"name": "Demo City",
"apiUrl": "https://demo.example.com",
"apiKey": "citizen_key",
"staffApiKey": "staff_key"
}
]'Tools accept an optional tenant parameter to target a specific tenant. Use
list_tenants (staff toolset) for discovery.
| Variable | Required | Default | Description |
|---|---|---|---|
GEOREPORT_SERVER_URL |
yes (single-tenant) | GeoReport v2 base URL | |
GEOREPORT_API_KEY |
yes (single-tenant) | Citizen API key | |
GEOREPORT_STAFF_API_KEY |
no | Staff API key for update/comment tools | |
TENANTS_CONFIG |
no | JSON array for multi-tenant mode (overrides single-tenant vars) | |
MCP_AUTH_USERS |
no | JSON array mapping Bearer tokens to scoped permissions | |
MCP_AUTH_TOKEN |
no | Legacy single shared MCP auth token (single-tenant only) | |
ENABLED_TOOLSETS |
no | read,intake |
Comma-separated toolset IDs to expose |
MCP_SERVER_PORT |
no | 3100 |
Express listen port |
PUBLIC_URL |
no | Base URL for upload link generation | |
UPLOAD_DIR |
no | /data/uploads |
Local filesystem path for photo store |
UPLOAD_STORAGE |
no | fs |
Storage backend (fs only) |
LOG_LEVEL |
no | info |
Winston log level |
| Method | Path | Purpose |
|---|---|---|
GET |
/health |
Docker healthcheck + uptime monitoring |
POST |
/api/mcp |
JSON-RPC MCP endpoint (requires Authorization: Bearer <token>) |
GET |
/api/upload/:token |
Browser upload page (one-time, 10 min TTL) |
POST |
/api/upload/:token |
Upload handler (base64 image body) |
Testing the MCP endpoint:
# Health check
curl https://<your-server-host>/health
# Tool listing (requires Bearer token)
curl -X POST https://<your-server-host>/api/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <YOUR_BEARER_TOKEN>" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'Production runs as a Docker Hardened Image (dhi.io/node:24), pnpm 11, non-root (UID 1000).
# Build (requires DHI subscription for dhi.io/node:24)
pnpm run build # tsup bundles src/server.ts -> dist/server.js
# Run with Docker
docker run --rm \
-p 3100:3100 \
-v mcp-uploads:/data/uploads \
-e TENANTS_CONFIG='...' \
-e MCP_AUTH_USERS='...' \
-e ENABLED_TOOLSETS=read,intake,reporting \
markaspot/markaspot-mcp:latestProduction deployments use deploy/docker-compose.mcp.yml behind Traefik with TLS.
Tenant env comes from tenants/<tenant>.mcp.env (chmod 600, not committed).
See deploy/mcp.env.example for a complete annotated environment template.
# Install dependencies
pnpm install
# Run in development mode (Express + SSE, dev-only)
pnpm run dev
# Build for production
pnpm run build # output: dist/server.js
# Type checking
pnpm run typecheck
# Linting
pnpm run lint
# Tests
pnpm test| Symptom | Check |
|---|---|
401 Unauthorized |
Bearer token missing or not in MCP_AUTH_USERS |
403 Forbidden |
Jurisdiction or toolset not in token's allowed scope |
405 Method Not Allowed |
Use POST for /api/mcp, not GET |
| Tool not visible | Check ENABLED_TOOLSETS and allowedToolsets on the token |
| Empty tool responses | Test GeoReport API directly; check GEOREPORT_SERVER_URL |
isAuthConfigured: false |
MCP_AUTH_USERS missing or empty; add at least one user |
- Bearer tokens are compared with
crypto.timingSafeEqualto prevent timing attacks. - Uploaded images are validated against file magic bytes (not client-claimed MIME type).
/uploadsserves files withContent-Disposition: attachmentandX-Content-Type-Options: nosniff.- CORS is enabled only on
/api/mcp; upload and health endpoints are same-origin-only. - No credentials are logged; the winston logger redacts secrets.
- HTTPS required for production.
This MCP server is part of the Mark-a-Spot ecosystem and is backend-agnostic: it works against any GeoReport v2 / Open311 compliant backend.
- markaspot/markaspot — the Drupal GeoReport v2 / Open311 backend this server talks to (GPL-2.0-or-later)
- CivicSpot — hosted, self-service edition of Mark-a-Spot; the quickest way to get a compatible GeoReport backend without self-hosting Drupal
The hosted MCP/chat integration with staff workflows is part of Mark-a-Spot Enterprise. See mark-a-spot.com.
Open source under the GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later). You may use, modify, and self-host markaspot-mcp freely. If you run a modified version as a network service, the AGPL requires you to make your modified source available to its users. See the LICENSE file.
Built and maintained by Civic Patches GmbH, Brühl, Germany. For commercial support, hosting, or the Mark-a-Spot Enterprise edition, see mark-a-spot.com.