mapsql-mcp

io.github.mveyseldelibas由 GitHub 帳號 mveyseldelibas 發布v0.2.0更新於 Nov 6, 2026

Safe MCP server for PostGIS and Oracle Spatial: query geodata and answer with maps.

概覽

AI 產生的概覽

針對 PostGIS 或 Oracle Spatial 的唯讀 MCP 伺服器,讓助理探索空間圖層、執行空間查詢並以地圖形式回傳結果。

可以做什麼
  • 探索並描述空間圖層、欄位、範圍與範例圖徵
  • 依屬性、距離、最近鄰、緩衝區或與另一圖層、bbox、GeoJSON 的空間關係篩選圖徵
  • 對圖層做彙總與空間連結,並計算邊界框
  • 在結構化工具無法表達問題時執行受保護的唯讀 SQL
  • 將結果繪製成含 OpenStreetMap 底圖的 PNG 地圖,也可輸出互動式 HTML
適用情境
適合已將地理資料存放在 PostGIS 或 Oracle Spatial,希望助理用自然語言回答位置相關問題並直接附上地圖,而不必手寫 PostGIS 或 SDO_* SQL 的情境。它是助理的建構元件,而非獨立應用程式。
執行需求
本機 Python 3.10+ 程序,通常以 uvx 或 pipx 執行。需要 PostGIS 或 Oracle Spatial 資料庫,連線字串放在 DATABASE_URL(Oracle 也可改用 MAPSQL_ORACLE_DSN、MAPSQL_ORACLE_USER 與 MAPSQL_ORACLE_PASSWORD,以及選用的 wallet 變數)。使用 OpenStreetMap 底圖需要網路,也可關閉以離線使用。
安裝前請注意
DATABASE_URL 與 Oracle 密碼類變數含資料庫憑證,應使用專用的唯讀使用者,切勿使用管理員帳號。查詢結果會傳送給助理所用的模型供應商,因此不要連接內容不得離開組織的資料庫。選用的 HTTP 傳輸沒有身分驗證,應只綁定本機回環位址或置於自有代理之後。寫入功能預設關閉,且僅 PostGIS 可開啟。

安裝

在 SourceWeft 中

  1. 開啟 儀表板中的 mapsql-mcp,將其新增到工作區。
  2. 為需要使用其工具的對話啟用該服務。

Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。

其他 MCP 客戶端

參照 儲存庫 中的啟動說明。

README

mapsql-mcp

Ask your PostGIS or Oracle Spatial database questions in plain English and get the answer back as a map.

mapsql-mcp is a Model Context Protocol server that lets AI assistants (Claude Desktop, Claude Code, Cursor, and any other MCP client) explore, query and analyse a PostGIS or an Oracle Spatial (SDO_GEOMETRY) database safely and answer with maps rendered right in the chat.

[License: MIT] [Python] [MCP] [status] [PostGIS] [Oracle Spatial]

[Pharmacies within 500 m of Kizilay]

"Which pharmacies are within 500 m of Kızılay square?" - the model discovers the layers, calls within_distance (geodesic metres, whatever the table's SRID), then render_map. The picture above is the literal output of those two tool calls on the bundled demo database.

[mapsql-mcp demo: question, tool calls, map]

The GIF is built by scripts/make_demo_video.py from real tool outputs on the demo databases (PostGIS and Oracle).

Türkçe: kullanım kılavuzu aşağıda · Who is it for / step-by-step: Usage

Why another PostGIS / Postgres / Oracle MCP server?

Generic Postgres or Oracle MCP servers are not spatial-aware: the model has to hand-write PostGIS / SDO_* SQL and gets SRIDs, geography casts, units and ST_DWithin / SDO_WITHIN_DISTANCE arguments wrong in subtle ways, then cannot show the result. mapsql-mcp is built around three ideas:

  1. Safe by construction. Every query is parsed (PostgreSQL: the real PostgreSQL parser; Oracle: sqlglot in Oracle mode), runs in a READ ONLY transaction with a timeout and a row cap, and secrets never reach the model. Writes are off unless you ask for them (PostGIS only).
  2. Spatial tools instead of SQL guessing. within_distance, nearest, buffer, spatial_filter, spatial_join, aggregate, bbox_of ... take care of SRIDs, metric units and indexes - the same 14 tools on both databases. Plain run_sql is still there.
  3. Answers are maps. render_map returns a PNG (OpenStreetMap basemap, choropleths, labels) as MCP image content and can also write an interactive Leaflet HTML file.

And the model is told to ask instead of guess: the server instructions, a prompt and the tools themselves refuse to proceed on "pharmacies nearby" (nearby what?) or on a place name that matches ten features.

Quickstart

Requires Python 3.10+ and a PostGIS or Oracle Spatial database. Use a read-only database role / user (see SECURITY.md). The backend is chosen from the connection string.

bash
pip install mapsql-mcp            # or: pipx install mapsql-mcp   |   uvx mapsql-mcpexport DATABASE_URL=postgresql://readonly:secret@localhost:5432/gismapsql-mcp --check                # prints PostgreSQL/PostGIS versions and exits
# Oracle Spatial instead (python-oracledb thin mode: no Oracle Client install needed) - see "Oracle Spatial" belowpip install "mapsql-mcp[oracle]"export DATABASE_URL="oracle://MAPSQL_RO:secret@dbhost:1521/ORCLPDB1"mapsql-mcp --check

Claude Desktop

Edit claude_desktop_config.json (macOS ~/Library/Application Support/Claude/, Windows %APPDATA%\Claude\):

json
{  "mcpServers": {    "postgis": {      "command": "uvx",      "args": ["mapsql-mcp"],      "env": { "DATABASE_URL": "postgresql://readonly:secret@localhost:5432/gis" }    }  }}

Claude Code

bash
claude mcp add postgis -e DATABASE_URL=postgresql://readonly:secret@localhost:5432/gis -- uvx mapsql-mcp

Cursor (.cursor/mcp.json) / Windsurf

json
{  "mcpServers": {    "postgis": {      "command": "uvx",      "args": ["mapsql-mcp", "--schemas", "public,osm"],      "env": { "DATABASE_URL": "postgresql://readonly:secret@localhost:5432/gis" }    }  }}

VS Code (.vscode/mcp.json, Copilot Chat in Agent mode)

VS Code uses a servers key and can prompt for the password instead of storing it in the file:

json
{  "inputs": [    { "id": "pg-url", "type": "promptString", "description": "PostGIS connection URL", "password": true }  ],  "servers": {    "postgis": {      "type": "stdio",      "command": "uvx",      "args": ["mapsql-mcp", "--schemas", "public,osm"],      "env": { "DATABASE_URL": "${input:pg-url}" }    }  }}

Other MCP clients (ChatGPT-compatible, remote)

Run the streamable-HTTP transport and point the client at http://127.0.0.1:8000/mcp:

bash
mapsql-mcp --transport streamable-http --host 127.0.0.1 --port 8000

There is no authentication on the HTTP transport: keep it on loopback or put it behind your own authenticating reverse proxy / tunnel. If you installed from a clone use "command": "python", "args": ["-m", "mapsql_mcp"].

Usage, step by step

Who it is for. GIS developers, database people, analysts and researchers who already use an AI assistant (Claude Code, Claude Desktop, Cursor, VS Code + Copilot) and have spatial data in PostGIS or Oracle Spatial. It is a building block for an assistant, not an end-user app: there is no web UI of its own, the chat window is the UI.

1. Install:

bash
pipx install "mapsql-mcp[oracle]"     # or: pip install "mapsql-mcp[oracle]"

2. Point it at a database with a read-only user and check the connection:

bash
export DATABASE_URL=postgresql://readonly:secret@localhost:5432/gis      # or oracle://USER:PASS@host:1521/servicemapsql-mcp --check

No database at hand? Build the bundled demo (Natural Earth + OpenStreetMap Ankara) in two minutes, see the next section.

3. Register it with your assistant. Put one of the JSON blocks from Quickstart into the client's config (.mcp.json for Claude Code, .vscode/mcp.json for VS Code, .cursor/mcp.json for Cursor, claude_desktop_config.json for Claude Desktop). In VS Code click Start above the server entry; in Claude Code run /mcp and check it says connected. You should see 14 tools.

4. Ask in plain language. For example:

  • "What layers do you have? Summarise them."
  • "Which pharmacies are within 500 m of Kızılay (39.9208, 32.8541)? Show them on a map."
  • "Which neighbourhood has the most cafes? Colour the neighbourhoods by count."
  • "The 5 hospitals nearest to this point, with distances."
  • "Find the pharmacies nearby." - a good assistant asks nearby what? instead of guessing.

5. See the map. render_map returns the PNG to the assistant and saves it (and optionally an interactive Leaflet HTML) under ~/mapsql-mcp-output. Some clients (e.g. the Claude Code panel in VS Code) do not show tool images to you; add MAPSQL_OPEN_MAPS=1 (or --open-maps) to the server's env and every map opens in your default viewer.

Good to know. The assistant's provider (Anthropic, OpenAI, ...) receives the rows the tools return, so do not connect databases whose content must not leave your organisation; use --schemas / --layers to expose only what is needed, and keep the database user read-only.

Try it with the demo database (2 minutes)

The demo data is Natural Earth (world countries, provinces, cities, rivers, lakes - public domain) plus an OpenStreetMap extract of central Ankara (POIs, roads, parks, 105 neighbourhoods - ODbL). A pinned snapshot ships in demo/data (2.8 MB), so building it works offline.

bash
docker run -d --name pg -p 55432:5432 -e POSTGRES_PASSWORD=pw postgis/postgis:16-3.4pip install -e .python demo/build_demo_db.py --database-url postgresql://postgres:pw@localhost:55432/postgresexport DATABASE_URL=postgresql://postgres:pw@localhost:55432/postgres

Then ask your assistant, for example:

  • "What layers do you have? Summarise the Ankara data."
  • "Which neighbourhood has the most pharmacies? Show it as a map."
  • "Cafes within 300 m of Kızılay (lat 39.9208, lon 32.8541), on a map."
  • "Find the pharmacies nearby." → it should ask where.
Cafes per neighbourhood (spatial_join + choropleth)5 nearest hospitals (nearest)
[cafes][hospitals]

Map data © OpenStreetMap contributors (ODbL). Interactive versions of two of them: docs/images/*.html (open in a browser).

Oracle Spatial

New in v0.2: the same tools, the same safety model and the same maps on Oracle Database with Spatial (19c, 21c, 23ai / 26ai, Autonomous Database, ...), through python-oracledb in thin mode (no Oracle Client libraries needed).

[Pharmacies within 500 m of Kizilay, Oracle Spatial]

Same question, same answer (20 pharmacies) as on PostGIS - this time from SDO_WITHIN_DISTANCE(... 'distance=500 unit=M') on an Autonomous Database. Map data (c) OpenStreetMap contributors (ODbL).

Install and connect

bash
pip install "mapsql-mcp[oracle]"        # adds oracledb + sqlglot
VariableMeaning
DATABASE_URL=oracle://USER:PASS@host:1521/service_nameEasy form. Percent-encode special characters in the password (@ -> %40). Add ?protocol=tcps (or ?tls=true) for TLS; other ?key=value pairs are passed on as Easy Connect parameters (e.g. ssl_server_dn_match=yes).
MAPSQL_ORACLE_DSN + MAPSQL_ORACLE_USER + MAPSQL_ORACLE_PASSWORDFor long TNS descriptors, tcps://host:1522/service strings or tnsnames.ora aliases (with TNS_ADMIN). The DSN overrides the host part of an oracle:// URL; user/password given here override those in the URL. Keeping the password out of the URL avoids quoting problems.
MAPSQL_ORACLE_WALLET_DIR, MAPSQL_ORACLE_WALLET_PASSWORDOnly for mutual-TLS (wallet) connections, e.g. Autonomous Database with "mTLS required".
MAPSQL_BACKEND=postgis|oracle / --backendForce the backend. Default: detected from the DATABASE_URL scheme, or Oracle when only MAPSQL_ORACLE_DSN is set.

All other settings (MAPSQL_SCHEMAS, MAPSQL_LAYERS, MAPSQL_STATEMENT_TIMEOUT_MS, MAPSQL_MAX_ROWS, ...) work unchanged; the timeout is applied as the connection call_timeout, schemas are Oracle owners.

Autonomous Database (TLS, no wallet). In the console open Database connection, choose TLS authentication: TLS (instead of mTLS; requires an access control list / allowed IPs for your address) and copy the Low/Medium connection string. The descriptor looks like (description=(address=(protocol=tcps)(port=1522)(host=...))(connect_data=(service_name=...))(security=(ssl_server_dn_match=yes))); put it in MAPSQL_ORACLE_DSN (it is long and contains parentheses, so it does not fit well in a URL). The server certificate is verified against the operating system's trust store. Use a dedicated read-only user, never ADMIN.

bash
export MAPSQL_ORACLE_DSN='(description=(retry_count=3)(address=(protocol=tcps)(port=1522)(host=adb.<region>.oraclecloud.com))(connect_data=(service_name=xxxx_mydb_low.adb.oraclecloud.com))(security=(ssl_server_dn_match=yes)))'export MAPSQL_ORACLE_USER=MAPSQL_RO MAPSQL_ORACLE_PASSWORD='...'mapsql-mcp --check

Read-only user

scripts/oracle_readonly_user.sql is the Oracle counterpart of readonly_role.sql: a user with only CREATE SESSION plus SELECT on the tables the assistant may see. The spatial metadata views, MDSYS.CS_SRS and the SDO_* operators are available to every user, so nothing else is needed.

Claude Desktop / Claude Code / Cursor / VS Code

json
{  "mcpServers": {    "oracle-spatial": {      "command": "uvx",      "args": ["--from", "mapsql-mcp[oracle]", "mapsql-mcp", "--schemas", "GIS"],      "env": { "DATABASE_URL": "oracle://MAPSQL_RO:secret@dbhost:1521/ORCLPDB1" }    }  }}
bash
claude mcp add oracle-spatial -e DATABASE_URL=oracle://MAPSQL_RO:secret@dbhost:1521/ORCLPDB1 -- uvx --from "mapsql-mcp[oracle]" mapsql-mcp --schemas GIS

VS Code (.vscode/mcp.json) - the password is prompted for and never stored in the file; a TNS descriptor goes into MAPSQL_ORACLE_DSN:

json
{  "inputs": [    { "id": "ora-pw", "type": "promptString", "description": "Oracle password for MAPSQL_RO", "password": true }  ],  "servers": {    "oracle-spatial": {      "type": "stdio",      "command": "uvx",      "args": ["--from", "mapsql-mcp[oracle]", "mapsql-mcp", "--schemas", "GIS", "--no-basemap"],      "env": {        "MAPSQL_ORACLE_DSN": "dbhost:1521/ORCLPDB1",        "MAPSQL_ORACLE_USER": "MAPSQL_RO",        "MAPSQL_ORACLE_PASSWORD": "${input:ora-pw}"      }    }  }}

Oracle demo schema (3 minutes)

scripts/oracle_demo_db.py loads the same Natural Earth + OpenStreetMap-Ankara data as the PostGIS demo into the schema MAPSQL_DEMO (WORLD_COUNTRIES, WORLD_CITIES, ANKARA_POIS, ANKARA_NEIGHBOURHOODS, ... - Oracle has no schemas-within-a-database convention like world.countries, so the PostGIS schema becomes a table-name prefix), registers USER_SDO_GEOM_METADATA, builds spatial indexes, adds TUREF TM30 (EPSG:5254, metres) copies of two Ankara layers to exercise projected coordinate systems, and creates the read-only user MAPSQL_RO. It needs an administrator login in a git-ignored ini file:

ini
; oracle-test.ini[oracle]user = ADMINpassword = ...dsn = (description=...)    ; or host:1521/service
bash
python scripts/oracle_demo_db.py          # creates MAPSQL_DEMO + MAPSQL_RO (random passwords are written to the same ini file)python scripts/make_oracle_demo_maps.py --ini oracle-test.ini     # the maps in docs/images/oracle_*.png
Cafes per neighbourhood (spatial_join + choropleth)Projected layer: TUREF TM30, SRID 5254
[cafes][tm30]

How the tools map to Oracle Spatial

ToolOracle implementation
list_layers / describe_layerALL_SDO_GEOM_METADATA (+ unregistered SDO_GEOMETRY columns of accessible tables/views), SRID, tolerance, geometry type from SDO_GTYPE, spatial-index presence/validity from ALL_INDEXES, CRS kind/name from MDSYS.SDO_COORD_REF_SYS / CS_SRS
within_distanceSDO_WITHIN_DISTANCE(geom, ref, 'distance=N unit=M'), distance via SDO_GEOM.SDO_DISTANCE(..., 'unit=M')
nearestSDO_NN + SDO_NN_DISTANCE (sdo_batch_size when attribute filters are present, so the filter is applied while neighbours stream out)
bufferSDO_GEOM.SDO_BUFFER(..., 'unit=M arc_tolerance=...'), dissolve with SDO_AGGR_UNION, area with SDO_AREA(..., 'unit=SQ_KM')
spatial_filter / spatial_joinSDO_RELATE masks (ANYINTERACT, INSIDE+COVEREDBY+EQUAL, CONTAINS+COVERS+EQUAL, TOUCH, OVERLAPBDYDISJOINT+OVERLAPBDYINTERSECT), SDO_WITHIN_DISTANCE for dwithin; joins run as a set-based spatial join grouped by ROWID
bbox_ofSDO_AGGR_MBR over SDO_CS.TRANSFORM(geom, 4326)
aggregateSDO_GEOM.SDO_AREA(... 'unit=SQ_KM') / SDO_LENGTH(... 'unit=KM')
GeoJSONSDO_UTIL.TO_GEOJSON on geometries transformed with SDO_CS.TRANSFORM(geom, 4326); simplified/rounded in Python
run_sqlguarded Oracle SQL; every SDO_GEOMETRY column is returned as GeoJSON (WGS84)

Metres are right on geodetic layers (EPSG:4326 and Oracle's legacy 8307: geodesic on the ellipsoid) and projected layers (planar in the projection, converted with unit=M). Values are always bind variables; identifiers come from the catalog.

Differences from PostGIS to know about

  • Spatial index required for SDO_* operators. Layers without a valid spatial index are queried with index-free SDO_GEOM.* functions (flagged in the response, and describe_layer warns). Create indexes (CREATE INDEX ... INDEXTYPE IS MDSYS.SPATIAL_INDEX_V2) and register the layer in USER_SDO_GEOM_METADATA for speed.
  • crosses has no faithful Oracle mask: spatial_filter refuses it with an explanation instead of returning a possibly wrong answer (use intersects/touches/overlaps, or run_sql with your own SDO_RELATE mask). within/contains include boundary contact (COVEREDBY/COVERS) and EQUAL; overlaps is OVERLAPBDYDISJOINT+OVERLAPBDYINTERSECT.
  • Write mode is not available on Oracle in this version (--allow-write is refused at start-up).
  • Identifiers are shown in lower case (mapsql_demo.ankara_pois, column name) when they are plain upper-case Oracle names; they remain valid unquoted in SQL. Quoted mixed-case names are shown as they are and must be quoted in run_sql.
  • Oracle stores the empty string as NULL. EQ '' filters are translated to IS NULL.
  • Small numeric differences to PostGIS (checked on the demo data): distances within 0.1 m for geodetic layers; areas/lengths within about 0.01 %; dwithin joins on polygons can differ by a feature at the boundary; a buffer's area differs by its polygon approximation. On a projected layer distances are planar in that projection (TM30 is within about 1 m per kilometre of geodesic distances near the central meridian).
  • GeoJSON conversion does not support circular arcs / compound curves (buffers of projected data are densified first); a geometry that Oracle cannot serialise makes the tool fail with an explicit database error.
  • list_layers reports extents with SDO_AGGR_MBR (skipped for layers over 500 000 rows); SDO_TUNE.EXTENT_OF cannot run inside a READ ONLY transaction.

Tools

ToolWhat it does
server_infoDatabase / PostGIS or Oracle Spatial versions, backend, mode, limits, allowlists
list_layersSpatial layers with geometry type, SRID, row estimate, extent, columns (searchable)
describe_layerColumns/types, exact count and extent, top values of text columns, numeric ranges
sample_featuresA few real features
find_featuresAttribute filters (= != < > like ilike contains in between is_null ...), ordering, column selection
within_distanceFeatures within N metres of a lon/lat or of other features; geodesic for lon/lat data, planar for metric CRSs, converted for feet etc.; adds distance_m
nearestk nearest neighbours with distance_m (GiST KNN pre-selection on large tables)
bufferGeodesic buffer of a point or a (filtered) layer, optionally dissolved, with area_km2
spatial_filterintersects / within / contains / touches / crosses / overlaps / disjoint vs another layer, a bbox or a GeoJSON geometry
aggregatecount, count_distinct, sum, avg, min, max, geodesic area_km2 / length_km, grouped
spatial_joinPer left feature: count / sum / avg / attach of related right features (intersects, within, contains, dwithin)
bbox_ofBounding box and centre of a layer or subset
run_sqlGuarded read-only SQL (PostgreSQL or Oracle dialect); geometry columns come back as GeoJSON
render_mapPNG map as MCP image content (+ saved file), optional Leaflet HTML; layers from result_ids, GeoJSON or SQL; labels and choropleth (value_property)
execute_sqlOnly with --allow-write, PostGIS only: one INSERT / UPDATE / DELETE (WHERE required) / CREATE TABLE|INDEX|VIEW / ALTER TABLE ADD COLUMN

All coordinates are WGS84 lon/lat; distances are metres; areas km². Every geometry-returning tool stores the full (simplified) result server-side and returns a result_id, so render_map(result_ids=[...]) never needs the model to copy big GeoJSON around. Responses stay small: geometries above a vertex budget are returned as representative points (with a note) unless you ask for geometry="full".

Prompts: gis_assistant, spatial_question, explore_database. Resources: mapsql://guide, mapsql://layers.

Ask, don't guess

within_distance with no point and no reference feature does not guess a location; it fails with "Missing reference: near WHAT? ... ask them". A reference that matches several features returns the candidates and asks which one is meant. Likewise a missing distance, an ambiguous layer name, a numeric filter given text, swapped lon/lat ... The server instructions (sent on connect) tell the model to ask one short clarifying question first. This was motivated by the author's TR-CBS-Komut benchmark, where models differed from 0% to 98% in recognising missing information.

Safety model

Defence in depth - no single layer is trusted (SECURITY.md has the full threat model):

LayerWhat it does
SQL parsingPostgreSQL: pglast (the actual PostgreSQL parser). Oracle: sqlglot (Oracle dialect) plus an independent token pass - exactly one statement, plain SELECT/WITH/set operations only; rejects DML/DDL/MERGE, PL/SQL blocks, EXECUTE IMMEDIATE, FOR UPDATE, database links (@), DBMS_*/UTL_*/CTX_* packages, SYS/DBA_*/V$* objects, sequences, URL/XML functions; anything unparseable is refused (see SECURITY.md). PostgreSQL details: Exactly one statement, and it must be a plain SELECT (CTEs/UNION/VALUES fine). Rejects DDL/DML/COPY/DO/SET/transaction control, SELECT INTO, FOR UPDATE, data-modifying CTEs, pg_read_file, lo_*, dblink*, pg_sleep, set_config, nextval, query_to_xml, all pg_* functions except a small harmless list, and credential/activity relations (pg_authid, pg_shadow, pg_stat_activity, pg_settings ...). 540+ test cases.
READ ONLY transactionPostgreSQL: every query runs in BEGIN READ ONLY with statement_timeout, lock_timeout, always rolled back; a SELECT my_volatile_function() that tries to INSERT is stopped by PostgreSQL (tested). Oracle: SET TRANSACTION READ ONLY plus call_timeout, always rolled back (ORA-01456 for writes, tested with a write-capable user).
Row capsServer-side cursor / FETCH FIRST + hard cap (MAPSQL_MAX_ROWS, default 5000); the server never materialises a huge result.
Allowlists--schemas public,osm and/or --layers 'osm.*,public.cities': hidden everywhere (catalog tools, SQL guard, search_path).
No secrets in outputErrors are rewritten to the primary message; DSNs, passwords and hosts are scrubbed; the connection string is never returned.
Writes off by default--allow-write adds one separate tool with a much narrower guard (no DROP/TRUNCATE/GRANT, no UPDATE/DELETE without WHERE).
Your database roleThe strongest layer: connect with a role/user that only has SELECT on what the assistant may see (scripts/readonly_role.sql, scripts/oracle_readonly_user.sql).

Configuration

Env varCLI flagDefaultMeaning
DATABASE_URL(--database-url, avoid)requiredpostgresql://... or oracle://USER:PASS@host:1521/service_name
MAPSQL_ORACLE_DSN, MAPSQL_ORACLE_USER, MAPSQL_ORACLE_PASSWORDOracle: DSN / TNS descriptor and credentials (alternative to the URL)
MAPSQL_ORACLE_WALLET_DIR, MAPSQL_ORACLE_WALLET_PASSWORDOracle: mTLS wallet (optional)
MAPSQL_BACKEND--backendautopostgis or oracle
MAPSQL_SCHEMAS--schemasallComma-separated schema allowlist (Oracle: owners)
MAPSQL_LAYERS--layersallComma-separated table/layer patterns (schema.table, schema.*, table)
MAPSQL_STATEMENT_TIMEOUT_MS--statement-timeout-ms15000Per-query timeout
MAPSQL_MAX_ROWS / MAPSQL_DEFAULT_LIMIT--max-rows5000 / 100Hard row cap / default limit
MAPSQL_ALLOW_WRITE--allow-writeoffEnable execute_sql (PostGIS only)
MAPSQL_BASEMAP--no-basemaposmosm, none, or a tile URL template with {z}/{x}/{y}
MAPSQL_OUTPUT_DIR--output-dir~/mapsql-mcp-outputWhere render_map saves PNG/HTML (+ tile cache)
MAPSQL_OPEN_MAPS--open-mapsoffOpen each rendered map in the default viewer (local use; for clients such as the Claude Code VS Code panel that do not display tool images)
MAPSQL_LOG_LEVELWARNINGLogs go to stderr only

Basemap tiles come from tile.openstreetmap.org with attribution, a descriptive User-Agent, a ≤ 16-tile cap per map and an on-disk cache. Please respect the OSM tile usage policy; for heavy use point MAPSQL_BASEMAP at your own tile server, or use none (works fully offline).

Benchmark: mapsql-bench

bench/ ships 60 English GIS questions over the demo database (52 answerable + 8 that must be answered with a clarifying question), in 8 categories (attribute, distance, spatial, aggregate, join, buffer, bbox, ambiguous), with gold answers computed by hand-written SQL, and a runner that evaluates any OpenAI-compatible model (OpenAI, OpenRouter, vLLM, Ollama, LM Studio ...) in an agent loop that calls this server's tools over stdio:

bash
python bench/run_bench.py --oracle                  # no LLM: replays one reference tool call per questionpython bench/run_bench.py --validate-gold           # re-checks gold answers against the databasepython bench/run_bench.py --model gpt-4o-mini --api-key-env OPENAI_API_KEYpython bench/run_bench.py --model qwen2.5:7b --base-url http://localhost:11434/v1

Scoring: numbers within a tolerance, names as case/accent-insensitive sets, and for ambiguous questions the model must end with CLARIFY: instead of ANSWER:. What is verified today (v0.1): the gold answers match the database (52/52) and the toolset can express every answerable question (oracle 52/52). No LLM results are published yet - the harness is ready; PRs adding model results to bench/RESULTS.md are very welcome.

Limitations (v0.2)

  • PostGIS or Oracle Spatial (one database per server); geometry/geography columns (PostGIS) or SDO_GEOMETRY columns (Oracle) of tables, views, materialised views, foreign tables. Oracle specifics: see Differences from PostGIS (read-only only, no crosses, index-free fallback).
  • No raster, topology or 3D support in the structured tools (run_sql can still query them).
  • The basemap needs internet (graceful fallback to a plain background) and supports OSM-style raster tiles only.
  • render_map draws one flat colour per layer, plus a single-variable choropleth; no custom styling yet.
  • The HTTP transport has no auth; execute_sql is a deliberately small write surface.
  • Cross-CRS joins transform the left geometry; very large cross-SRID dwithin joins can be slow.
  • Tested against PostgreSQL 18 / PostGIS 3.6 and Oracle Database 26ai (23.26, Spatial 23.7, Autonomous Database Always Free) with mcp 1.30 and 2.3 (the code supports both SDK generations); CI runs PostGIS 3.4 / PostgreSQL 16 and the Oracle guard tests (the database-backed Oracle tests skip without credentials).

Roadmap

  • v0.3: Oracle write mode (narrow guard), Oracle SDO_JOIN/Spatial Studio extras, auth for HTTP (bearer tokens), per-tool allowlists, vector-tile/PMTiles basemaps, raster/H3 summaries, an explain tool, routing helpers (pgRouting), result export (GeoJSON/GeoPackage), richer map styling
  • v0.4: multi-database profiles, MCP elicitation for clarifying questions (instead of plain-text asks), catalog caching knobs
  • Published benchmark results for popular models + a public leaderboard

Development

bash
python -m venv .venv && .venv/bin/pip install -e ".[dev]"# a throwaway PostGIS (never point the tests at a database you care about):docker run -d -p 55432:5432 -e POSTGRES_PASSWORD=pw -e POSTGRES_DB=mapsql_test postgis/postgis:16-3.4export MAPSQL_TEST_DATABASE_URL=postgresql://postgres:pw@localhost:55432/mapsql_testpytest                      # guard tests need no database; the rest recreate schemas `mt` and `mt_other`

Oracle tests (pytest -m oracle) run when oracle-test.ini ([oracle_ro], see above) or MAPSQL_TEST_ORACLE_URL / MAPSQL_TEST_ORACLE_DSN+_USER+_PASSWORD is available and scripts/oracle_demo_db.py has been run; otherwise they are skipped. The Oracle SQL guard tests need no database.

See CONTRIBUTING.md. Windows helper for a local cluster: scripts/dev_cluster.ps1.

Türkçe

mapsql-mcp, yapay zekâ asistanlarının (Claude Desktop/Code, Cursor, ChatGPT uyumlu MCP istemcileri) bir PostGIS veya Oracle Spatial (v0.2) veritabanını güvenli biçimde sorgulamasını, mekânsal analiz yapmasını ve cevabı harita olarak sohbette göstermesini sağlayan açık kaynaklı bir MCP sunucusudur.

  • Güvenlik önce: her sorgu gerçek PostgreSQL ayrıştırıcısı (pglast) ile doğrulanır; tek SELECT, READ ONLY işlem, zaman aşımı ve satır sınırı. pg_read_file, COPY, dblink, lo_* vb. engellidir. Yazma varsayılan olarak kapalıdır (--allow-write). Şema/katman izin listesi (--schemas, --layers) desteklenir; parola ve bağlantı dizesi çıktıya sızmaz.
  • Mekânsal araçlar: list_layers, describe_layer, within_distance (metre cinsinden, SRID'den bağımsız), nearest, buffer, spatial_filter, aggregate, spatial_join, bbox_of, run_sql ve render_map (OSM altlıklı PNG + isteğe bağlı etkileşimli Leaflet HTML, kloropet harita).
  • Belirsizse sorar: "yakındaki eczaneler" gibi eksik bilgili sorularda model tahmin yürütmek yerine "neye yakın?" diye sorar (araçlar da eksik girdiyi reddeder). Bu, yazarın TR-CBS-Komut kıyaslamasından çıkan bir derstir.
  • Kıyaslama: bench/ altında demo veritabanı üzerinde 60 İngilizce CBS sorusu (8'i "sormalı" tipi), altın cevaplar ve herhangi bir OpenAI uyumlu modeli deneyen çalıştırıcı bulunur.

Oracle Spatial: pip install "mapsql-mcp[oracle]" ve DATABASE_URL=oracle://KULLANICI:SIFRE@sunucu:1521/servis_adi (uzun TNS tanımlayıcıları için MAPSQL_ORACLE_DSN + MAPSQL_ORACLE_USER + MAPSQL_ORACLE_PASSWORD). Aynı 14 araç; mesafeler unit=M ile metre cinsinden, coğrafi (4326) ve projeksiyonlu (ör. TUREF TM30, EPSG:5254) katmanlarda doğrudur. Sorgular sqlglot (Oracle lehçesi) ile doğrulanır, SET TRANSACTION READ ONLY ve salt okunur kullanıcı (scripts/oracle_readonly_user.sql) önerilir. Ayrıntı: yukarıdaki Oracle Spatial bölümü.

Kimin için? Yapay zekâ asistanı (Claude Code, Claude Desktop, Cursor, VS Code + Copilot) kullanan ve mekânsal verisi PostGIS ya da Oracle Spatial'da duran CBS geliştiricileri, veritabanı uzmanları, analistler ve araştırmacılar için. Son kullanıcı uygulaması değildir; kendi ekranı yoktur, arayüzü sohbet penceresinin kendisidir.

Adım adım kullanım

1. Kurulum:

bash
pipx install "mapsql-mcp[oracle]"     # ya da: pip install "mapsql-mcp[oracle]"

2. Veritabanını salt okunur bir kullanıcıyla bağlayın ve bağlantıyı deneyin:

bash
export DATABASE_URL=postgresql://salt_okunur:sifre@localhost:5432/cbs     # ya da oracle://KULLANICI:SIFRE@sunucu:1521/servismapsql-mcp --check

Salt okunur kullanıcı betikleri: scripts/readonly_role.sql (PostGIS), scripts/oracle_readonly_user.sql (Oracle). Elinizde veritabanı yoksa yukarıdaki Try it with the demo database bölümüyle iki dakikada demo veritabanı kurulur (Natural Earth + OpenStreetMap Ankara).

3. Asistana tanıtın. Quickstart bölümündeki JSON örneklerinden birini istemcinin ayar dosyasına koyun: Claude Code için .mcp.json, VS Code için .vscode/mcp.json, Cursor için .cursor/mcp.json, Claude Desktop için claude_desktop_config.json. VS Code'da sunucunun üstündeki Start'a basın; Claude Code'da /mcp yazıp connected olduğunu görün. 14 araç listelenmelidir.

4. Türkçe sorun. Örnekler:

  • "Hangi katmanlar var? Özetle."
  • "Kızılay'a (39.9208, 32.8541) 500 m içindeki eczaneleri haritada göster."
  • "En çok kafesi olan mahalle hangisi? Mahalleleri kafe sayısına göre renklendir."
  • "Bu noktaya en yakın 5 hastaneyi mesafeleriyle listele."
  • "Yakındaki eczaneleri bul." — iyi bir asistan tahmin etmez, "neye yakın?" diye sorar.

5. Haritayı görün. render_map haritayı asistana PNG olarak döner ve ~/mapsql-mcp-output klasörüne kaydeder (istenirse etkileşimli Leaflet HTML de). Bazı istemciler (ör. VS Code'daki Claude Code paneli) aracın resmini size göstermez; sunucunun env kısmına MAPSQL_OPEN_MAPS=1 (ya da --open-maps) ekleyin, her harita varsayılan görüntüleyicide açılır.

Bilmeniz gereken. Asistanın sağlayıcısı (Anthropic, OpenAI vb.) araçların döndürdüğü satırları görür. Kurum dışına çıkmaması gereken verinin bulunduğu veritabanlarını bağlamayın; --schemas / --layers ile yalnız gereken katmanları açın ve veritabanı kullanıcısını salt okunur tutun. Demo veri: Natural Earth (kamu malı) ve OpenStreetMap (ODbL), ayrıntı: demo/ATTRIBUTION.md.

Yazar: Mehmet Veysel Delibaş, CBS yazılım mühendisi (Türkiye).

License and attribution

Code: MIT © 2026 Mehmet Veysel Delibaş. Demo data: Natural Earth (public domain) and OpenStreetMap (© OpenStreetMap contributors, ODbL); see demo/ATTRIBUTION.md. Maps rendered with the OSM basemap carry the required attribution.

來源:README.md,提交 da53da3

工具

14

server_info

Server status: PostgreSQL/PostGIS versions, mode (read-only), limits, allowlists. Call once if unsure what is available.

無需輸入參數。

list_layers

START HERE. List the spatial layers (geometry/geography tables & views) with geometry type, SRID, approx. row count, extent and column names. Use `search` to find layers by name/column (e.g. 'road', 'population').

輸入參數schemasearch

describe_layer

Describe one layer in detail: all columns and types, exact row count, extent, geometry types, the most common values of text columns and min/max of numeric columns. Call this BEFORE filtering so you use real column names and values.

輸入參數layer必填include_value_samples

sample_features

Return a few example features of a layer (random for small tables) to see real data.

輸入參數nlayer必填geometry

find_features

Find features by attribute filters (no spatial predicate). Returns GeoJSON + result_id for render_map. Use describe_layer first to learn column names and values.

輸入參數layer必填limitcolumnsfiltersgeometryorder_bydescending

within_distance

Features of `layer` within `distance_m` METRES of a reference: a lon/lat point OR feature(s) from another layer. Distances are geodesic/metric regardless of the layer's SRID. Results include distance_m, nearest first. If the user did not say what to measure from, or the reference matches several features, ASK the user.

輸入參數latlonlayer必填limitcolumnsfiltersgeometrydistance_m必填reference_layeruse_all_matchesreference_filters

nearest

The k nearest features of `layer` to a reference (lon/lat point or feature(s) of another layer), with distance_m in metres. Optional max_distance_m. If the user did not say what 'nearest' is relative to, ASK.

輸入參數klatlonlayer必填columnsfiltersgeometrymax_distance_mreference_layeruse_all_matchesreference_filters

buffer

Buffer geometries by a distance in METRES (geodesic). Buffer a point (lon/lat) or the features of a layer (+filters). dissolve=true merges into one polygon (also returns its area_km2). Returns GeoJSON + result_id.

輸入參數latlonlayerlimitfiltersdissolvedistance_m必填

spatial_filter

Spatial predicate filter: features of `layer` that intersect / are within / contain / touch / cross / overlap / are disjoint from (a) features of another layer (other_layer + other_filters), (b) a bbox [min_lon,min_lat,max_lon,max_lat], or (c) a GeoJSON geometry. Example: pois within the polygon 'Central Park'. Ask the user if the second operand is unclear.

輸入參數bboxlayer必填limitcolumnsfiltersgeometryrelationother_layerother_filtersgeojson_geometry

bbox_of

Bounding box (WGS84) and centre of a layer or a filtered subset. Useful for framing maps or checking coverage.

輸入參數layer必填filters

aggregate

Group-by statistics over a layer: count, count_distinct, sum, avg, min, max, area_km2 (geodesic polygon area), length_km (geodesic line length), optionally grouped by columns and filtered. Returns a table (no geometry).

輸入參數layer必填limitfiltersmetricsgroup_byorder_bydescending

spatial_join

Spatial join between two layers, per feature of the LEFT layer. mode='count' (default) counts related RIGHT features (e.g. pharmacies per district: left=districts, right=pois, relation='contains'); 'sum'/'avg' aggregate a numeric right_column; 'attach' lists matched pairs with right_columns. relation: intersects | within | contains | dwithin (needs distance_m). Returns the LEFT geometry so it can be mapped (choropleth-like).

輸入參數modelimitgeometryrelationmin_countdistance_mleft_layer必填order_descright_layer必填left_filtersright_columnright_columns另有 1 個

run_sql

Run a custom READ-ONLY SQL query (single SELECT, PostgreSQL/PostGIS syntax) when the structured tools do not fit. Validated by a SQL parser, executed in a READ ONLY transaction with a timeout and row cap. Geometry columns are returned as GeoJSON (EPSG:4326). Prefer list_layers/describe_layer first; alias every column.

輸入參數limitquery必填geometry

render_map

Draw a MAP and return it as a PNG image (with an OpenStreetMap basemap when online). Pass result_ids from earlier tools (find_features, within_distance, nearest, buffer, spatial_join, bbox_of, run_sql ...) - each becomes a coloured layer - or a GeoJSON string/object, or a SQL query that returns a geometry column. Also saves the PNG and, with save_html=true, an interactive Leaflet HTML map to disk. Use this whenever the answer is spatial.

輸入參數sqltitlewidthheightbasemapgeojsonsave_htmlresult_idslabel_propertyvalue_property

版本歷史

1
  1. v0.2.0最新Nov 5, 2026

評分

已滿足適用檢查項的 90%

該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。

  • 近 30 天內可連通25 分
  • 已宣告認證方式10 分

    請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。

  • 已列出工具必需15 分
  • 描述必需15 分
  • 圖示10 分
  • 文件15 分
  • 授權條款10 分