plannexus-mcp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ dist/
2
+ build/
3
+ *.egg-info/
4
+ __pycache__/
5
+ .venv/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PlanNexus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.4
2
+ Name: plannexus-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for PlanNexus — query UK planning-application data from Claude, ChatGPT or any MCP client.
5
+ Project-URL: Homepage, https://plannexus.io
6
+ Project-URL: Documentation, https://plannexus.io/mcp
7
+ Author: PlanNexus
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: claude,llm,mcp,plannexus,planning,planning-applications,uk
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Scientific/Engineering :: GIS
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: httpx>=0.27
17
+ Requires-Dist: mcp>=1.2.0
18
+ Description-Content-Type: text/markdown
19
+
20
+ # plannexus-mcp
21
+
22
+ Put **UK planning data inside your AI.** `plannexus-mcp` is an [MCP](https://modelcontextprotocol.io)
23
+ server that lets Claude, ChatGPT or any MCP client query [PlanNexus](https://plannexus.io) —
24
+ planning applications across ~363 UK local planning authorities — in plain English.
25
+
26
+ Ask things like *"what's been applied for within 500m of SW1A 1AA this month?"* and your
27
+ assistant calls PlanNexus directly.
28
+
29
+ ## Tools
30
+
31
+ | Tool | What it does |
32
+ |------|--------------|
33
+ | `search_applications` | Search by free text, postcode, council, or lat/lng + radius, with type/status/date filters |
34
+ | `get_application` | Full detail for one application — status, decision, dates, and (on a paid plan) **agent / applicant / case-officer contacts** |
35
+ | `get_constraints` | Planning constraints at a postcode or point — conservation area, listed building, Article 4, TPO, flood, green belt, … |
36
+ | `find_authority` | Look up a council's id by name |
37
+
38
+ ## Setup (Claude Desktop)
39
+
40
+ Add to `claude_desktop_config.json`:
41
+
42
+ ```json
43
+ {
44
+ "mcpServers": {
45
+ "plannexus": {
46
+ "command": "uvx",
47
+ "args": ["plannexus-mcp"],
48
+ "env": { "PLANNEXUS_API_KEY": "pn_live_..." }
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ Restart Claude and ask away. **Search works without a key** (public preview); add a
55
+ [PlanNexus API key](https://plannexus.io) to unlock applicant/agent contacts on a paid plan.
56
+
57
+ ## Run directly
58
+
59
+ ```bash
60
+ PLANNEXUS_API_KEY=pn_live_... uvx plannexus-mcp
61
+ ```
62
+
63
+ `PLANNEXUS_BASE_URL` overrides the API base (defaults to `https://api.plannexus.io/v1`).
64
+
65
+ ## Licence
66
+
67
+ MIT
@@ -0,0 +1,48 @@
1
+ # plannexus-mcp
2
+
3
+ Put **UK planning data inside your AI.** `plannexus-mcp` is an [MCP](https://modelcontextprotocol.io)
4
+ server that lets Claude, ChatGPT or any MCP client query [PlanNexus](https://plannexus.io) —
5
+ planning applications across ~363 UK local planning authorities — in plain English.
6
+
7
+ Ask things like *"what's been applied for within 500m of SW1A 1AA this month?"* and your
8
+ assistant calls PlanNexus directly.
9
+
10
+ ## Tools
11
+
12
+ | Tool | What it does |
13
+ |------|--------------|
14
+ | `search_applications` | Search by free text, postcode, council, or lat/lng + radius, with type/status/date filters |
15
+ | `get_application` | Full detail for one application — status, decision, dates, and (on a paid plan) **agent / applicant / case-officer contacts** |
16
+ | `get_constraints` | Planning constraints at a postcode or point — conservation area, listed building, Article 4, TPO, flood, green belt, … |
17
+ | `find_authority` | Look up a council's id by name |
18
+
19
+ ## Setup (Claude Desktop)
20
+
21
+ Add to `claude_desktop_config.json`:
22
+
23
+ ```json
24
+ {
25
+ "mcpServers": {
26
+ "plannexus": {
27
+ "command": "uvx",
28
+ "args": ["plannexus-mcp"],
29
+ "env": { "PLANNEXUS_API_KEY": "pn_live_..." }
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ Restart Claude and ask away. **Search works without a key** (public preview); add a
36
+ [PlanNexus API key](https://plannexus.io) to unlock applicant/agent contacts on a paid plan.
37
+
38
+ ## Run directly
39
+
40
+ ```bash
41
+ PLANNEXUS_API_KEY=pn_live_... uvx plannexus-mcp
42
+ ```
43
+
44
+ `PLANNEXUS_BASE_URL` overrides the API base (defaults to `https://api.plannexus.io/v1`).
45
+
46
+ ## Licence
47
+
48
+ MIT
@@ -0,0 +1,5 @@
1
+ """PlanNexus MCP server — query UK planning data from any MCP client."""
2
+
3
+ from plannexus_mcp.server import http_app, main, mcp
4
+
5
+ __all__ = ["http_app", "main", "mcp"]
@@ -0,0 +1,210 @@
1
+ """PlanNexus MCP server — query UK planning data from any MCP client.
2
+
3
+ A thin wrapper over the public PlanNexus REST API that exposes planning search,
4
+ application detail, point-constraints and authority lookup as MCP tools, so a
5
+ user can ask their AI assistant (Claude Desktop, Claude Code, ChatGPT, …) things
6
+ like "what's been applied for within 500m of SW1A 1AA this month?" and have it
7
+ call PlanNexus directly.
8
+
9
+ Auth: set ``PLANNEXUS_API_KEY``. The key is forwarded on every call, so paid
10
+ features (applicant/agent/case-officer contacts) appear automatically for a paid
11
+ key and are redacted for a free key — identical gating to the API. Search works
12
+ even with no key (public preview), so it's useful before sign-up.
13
+
14
+ Two transports from one codebase:
15
+ - ``main()`` — stdio, for a *local* client (the ``plannexus-mcp`` console script).
16
+ The key comes from ``PLANNEXUS_API_KEY``.
17
+ - ``http_app()`` — a streamable-HTTP ASGI app for the *hosted* PlanNexus MCP
18
+ endpoint, where auth middleware calls ``set_request_key()`` per request so one
19
+ process serves many users' keys.
20
+
21
+ Local setup (Claude Desktop ``claude_desktop_config.json``)::
22
+
23
+ {
24
+ "mcpServers": {
25
+ "plannexus": {
26
+ "command": "uvx",
27
+ "args": ["plannexus-mcp"],
28
+ "env": {"PLANNEXUS_API_KEY": "pn_live_..."}
29
+ }
30
+ }
31
+ }
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import contextvars
37
+ import os
38
+ from typing import Any
39
+
40
+ import httpx
41
+ from mcp.server.fastmcp import FastMCP
42
+
43
+ mcp = FastMCP("PlanNexus")
44
+
45
+ _DEFAULT_BASE = "https://api.plannexus.io/v1"
46
+ _TIMEOUT = httpx.Timeout(30.0, connect=10.0)
47
+
48
+ # The caller's API key. The local (stdio) server reads PLANNEXUS_API_KEY; the
49
+ # hosted (HTTP) server sets it per-request via this context var (one process,
50
+ # many users). _key() prefers the per-request value, else the env var.
51
+ _request_key: contextvars.ContextVar[str | None] = contextvars.ContextVar(
52
+ "plannexus_api_key", default=None
53
+ )
54
+
55
+
56
+ def set_request_key(key: str | None) -> None:
57
+ """Set the PlanNexus API key for the current request context (hosted server)."""
58
+ _request_key.set(key)
59
+
60
+
61
+ def _base_url() -> str:
62
+ return os.environ.get("PLANNEXUS_BASE_URL", _DEFAULT_BASE).rstrip("/")
63
+
64
+
65
+ def _key() -> str:
66
+ return _request_key.get() or os.environ.get("PLANNEXUS_API_KEY", "")
67
+
68
+
69
+ def _get(path: str, params: dict[str, Any]) -> Any:
70
+ """GET a PlanNexus endpoint, forwarding the API key, with LLM-friendly errors."""
71
+ clean = {k: v for k, v in params.items() if v not in (None, "", [])}
72
+ headers = {"Accept": "application/json", "User-Agent": "plannexus-mcp/0.1"}
73
+ key = _key()
74
+ if key:
75
+ headers["X-API-Key"] = key
76
+ try:
77
+ with httpx.Client(base_url=_base_url(), headers=headers, timeout=_TIMEOUT) as client:
78
+ r = client.get(path, params=clean)
79
+ except httpx.HTTPError as exc:
80
+ return {"error": f"request_failed: {exc}"}
81
+
82
+ if r.status_code == 401:
83
+ return {
84
+ "error": "Unauthorised — set a valid PlanNexus API key (get one at https://plannexus.io)."
85
+ }
86
+ if r.status_code == 402:
87
+ return {"error": "This is a paid PlanNexus feature — upgrade your plan to access it."}
88
+ if r.status_code == 404:
89
+ return {"error": "Not found."}
90
+ if r.status_code == 429:
91
+ return {"error": "Rate limit or monthly quota reached for your plan."}
92
+ if r.status_code >= 400:
93
+ return {"error": f"api_error {r.status_code}", "detail": r.text[:300]}
94
+ try:
95
+ return r.json()
96
+ except ValueError:
97
+ return {"error": "non_json_response", "detail": r.text[:300]}
98
+
99
+
100
+ @mcp.tool()
101
+ def search_applications(
102
+ query: str = "",
103
+ postcode: str = "",
104
+ authority_id: str = "",
105
+ lat: float | None = None,
106
+ lng: float | None = None,
107
+ radius_m: int = 1000,
108
+ application_type: str = "",
109
+ status: str = "",
110
+ date_received_from: str = "",
111
+ date_received_to: str = "",
112
+ limit: int = 20,
113
+ ) -> Any:
114
+ """Search UK planning applications across ~363 local planning authorities.
115
+
116
+ Combine any of:
117
+ - query: free text matched against description + address
118
+ - postcode: full or partial UK postcode (prefix match, e.g. "SW1A" or "SW1A 1AA")
119
+ - authority_id: a council UUID — use find_authority to look one up by name
120
+ - lat + lng + radius_m: everything within radius_m metres of a point
121
+ - application_type: e.g. "householder", "full", "outline", "listed_building"
122
+ - status: e.g. "approved", "refused", "pending"
123
+ - date_received_from / date_received_to: ISO dates (YYYY-MM-DD)
124
+
125
+ Returns a page of application summaries (id, reference, address, type, status,
126
+ dates, council, coordinates). Pass an id to get_application for full detail.
127
+ """
128
+ return _get(
129
+ "/applications",
130
+ {
131
+ "q": query,
132
+ "postcode": postcode,
133
+ "authority_id": authority_id,
134
+ "lat": lat,
135
+ "lng": lng,
136
+ "radius": radius_m,
137
+ "application_type": application_type,
138
+ "status": status,
139
+ "date_received_from": date_received_from,
140
+ "date_received_to": date_received_to,
141
+ "per_page": min(max(limit, 1), 100),
142
+ },
143
+ )
144
+
145
+
146
+ @mcp.tool()
147
+ def get_application(application_id: str) -> Any:
148
+ """Full detail for one planning application by id.
149
+
150
+ Returns address, description, type, status, decision, all dates, ward/parish,
151
+ coordinates, EPC (where matched) and — on a paid PlanNexus plan — the
152
+ applicant, agent and case-officer contact details (redacted on the free tier,
153
+ with a ``redacted_fields`` list). Get the id from search_applications.
154
+ """
155
+ return _get(f"/applications/{application_id}", {})
156
+
157
+
158
+ @mcp.tool()
159
+ def get_constraints(postcode: str = "", lat: float | None = None, lng: float | None = None) -> Any:
160
+ """Planning constraints at a point — by UK postcode, or by lat + lng.
161
+
162
+ Returns which designations apply at that location: conservation area, listed
163
+ building, Article 4 direction, tree preservation order (TPO), flood risk zone,
164
+ green belt, scheduled monument, AONB, SSSI, world heritage site, and more.
165
+ Answers "what would affect a development at this address?".
166
+ """
167
+ return _get("/constraints", {"postcode": postcode, "lat": lat, "lng": lng})
168
+
169
+
170
+ @mcp.tool()
171
+ def find_authority(name: str = "") -> Any:
172
+ """Find UK local planning authorities (councils) and their ids.
173
+
174
+ Pass a name or partial name (e.g. "camden", "north lincolnshire") to filter,
175
+ or omit to list all active councils. Use the returned id as ``authority_id``
176
+ in search_applications to scope a search to one council.
177
+ """
178
+ data = _get("/authorities", {})
179
+ if isinstance(data, dict) and isinstance(data.get("data"), list):
180
+ rows = data["data"]
181
+ if name:
182
+ needle = name.lower()
183
+ rows = [r for r in rows if needle in (r.get("name") or "").lower()]
184
+ return {
185
+ "authorities": [
186
+ {"id": r.get("id"), "name": r.get("name"), "gss_code": r.get("gss_code")}
187
+ for r in rows
188
+ ],
189
+ "total": len(rows),
190
+ }
191
+ return data
192
+
193
+
194
+ def main() -> None:
195
+ """Console entry point (``plannexus-mcp``) — serves the tools over stdio."""
196
+ mcp.run()
197
+
198
+
199
+ def http_app():
200
+ """Streamable-HTTP ASGI app for the hosted PlanNexus MCP endpoint.
201
+
202
+ The hosted deployment wraps this with middleware that reads the caller's
203
+ PlanNexus API key from the request and calls ``set_request_key()``, so a
204
+ single instance serves many users. (The local server uses ``main()`` instead.)
205
+ """
206
+ return mcp.streamable_http_app()
207
+
208
+
209
+ if __name__ == "__main__":
210
+ main()
@@ -0,0 +1,33 @@
1
+ [project]
2
+ name = "plannexus-mcp"
3
+ version = "0.1.0"
4
+ description = "MCP server for PlanNexus — query UK planning-application data from Claude, ChatGPT or any MCP client."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "PlanNexus" }]
9
+ keywords = ["mcp", "planning", "uk", "planning-applications", "plannexus", "llm", "claude"]
10
+ classifiers = [
11
+ "Programming Language :: Python :: 3",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Operating System :: OS Independent",
14
+ "Topic :: Scientific/Engineering :: GIS",
15
+ ]
16
+ dependencies = [
17
+ "mcp>=1.2.0",
18
+ "httpx>=0.27",
19
+ ]
20
+
21
+ [project.urls]
22
+ Homepage = "https://plannexus.io"
23
+ Documentation = "https://plannexus.io/mcp"
24
+
25
+ [project.scripts]
26
+ plannexus-mcp = "plannexus_mcp.server:main"
27
+
28
+ [build-system]
29
+ requires = ["hatchling"]
30
+ build-backend = "hatchling.build"
31
+
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["plannexus_mcp"]