ng-postcode-mcp 0.2.0__tar.gz → 0.2.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ng-postcode-mcp
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: MCP server for Nigeria's NIPOST digital postcode (NDAPS): validate, look up, reverse-geocode and resolve addresses to postcodes.
5
5
  Project-URL: Repository, https://github.com/Adeniyikayodee/ng-postcode
6
6
  Project-URL: Issues, https://github.com/Adeniyikayodee/ng-postcode/issues
@@ -22,8 +22,8 @@ Classifier: Topic :: Scientific/Engineering :: GIS
22
22
  Classifier: Typing :: Typed
23
23
  Requires-Python: >=3.10
24
24
  Requires-Dist: mcp<3,>=2.3
25
- Requires-Dist: ng-address-resolver<0.2,>=0.1
26
- Requires-Dist: ng-postcode[client]<0.2,>=0.1
25
+ Requires-Dist: ng-address-resolver<0.2,>=0.1.1
26
+ Requires-Dist: ng-postcode[client]<0.3,>=0.2
27
27
  Description-Content-Type: text/markdown
28
28
 
29
29
  # ng-postcode-mcp
@@ -58,19 +58,21 @@ The assistant reads the address and passes its landmarks and map searches to the
58
58
  | A street only | District code, low confidence |
59
59
  | A town only, or nothing found | No code, plus a question |
60
60
 
61
- Text alone rarely identifies a building, so ask users for a location pin when the exact building matters. This tool is pre-release: its NIPOST steps have not yet been run against the live API.
61
+ Text alone rarely identifies a building, so ask users for a location pin when the exact building matters. This tool is pre-release: it works against the live API, but its accuracy on real addresses is unmeasured.
62
62
 
63
63
  ## Install
64
64
 
65
- Requires [uv](https://docs.astral.sh/uv/). Get an API key from the [NIPOST developer dashboard](https://dashboard.postcode.gov.ng); `validate_postcode` works without one.
65
+ Works with any MCP client. The server runs over stdio:
66
66
 
67
- **Claude Code**
67
+ | Setting | Value |
68
+ | --- | --- |
69
+ | Command | `uvx` |
70
+ | Arguments | `ng-postcode-mcp` |
71
+ | Environment | `NG_POSTCODE_API_KEY` (optional for `validate_postcode`) |
68
72
 
69
- ```sh
70
- claude mcp add ng-postcode -e NG_POSTCODE_API_KEY=nipost_live_... -- uvx ng-postcode-mcp
71
- ```
73
+ It needs [uv](https://docs.astral.sh/uv/) installed. Get an API key from the [NIPOST developer dashboard](https://dashboard.postcode.gov.ng).
72
74
 
73
- **Claude Desktop, Cursor and other clients** that use an `mcpServers` config:
75
+ Most clients take this entry in their MCP settings:
74
76
 
75
77
  ```json
76
78
  {
@@ -84,6 +86,15 @@ claude mcp add ng-postcode -e NG_POSTCODE_API_KEY=nipost_live_... -- uvx ng-post
84
86
  }
85
87
  ```
86
88
 
89
+ | Client | How to add it |
90
+ | --- | --- |
91
+ | Claude Code | `claude mcp add ng-postcode -e NG_POSTCODE_API_KEY=nipost_live_... -- uvx ng-postcode-mcp` |
92
+ | Claude Desktop | The entry above, in its MCP server settings |
93
+ | Codex | `codex mcp add ng-postcode --env NG_POSTCODE_API_KEY=nipost_live_... -- uvx ng-postcode-mcp` |
94
+ | Cursor | The entry above, in `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global) |
95
+ | VS Code | The same server object in `.vscode/mcp.json`, under a top-level `"servers"` key instead of `"mcpServers"` |
96
+ | Others | Any client that launches stdio servers: use the command, arguments and environment above |
97
+
87
98
  ## Configuration
88
99
 
89
100
  | Variable | Default | Purpose |
@@ -30,19 +30,21 @@ The assistant reads the address and passes its landmarks and map searches to the
30
30
  | A street only | District code, low confidence |
31
31
  | A town only, or nothing found | No code, plus a question |
32
32
 
33
- Text alone rarely identifies a building, so ask users for a location pin when the exact building matters. This tool is pre-release: its NIPOST steps have not yet been run against the live API.
33
+ Text alone rarely identifies a building, so ask users for a location pin when the exact building matters. This tool is pre-release: it works against the live API, but its accuracy on real addresses is unmeasured.
34
34
 
35
35
  ## Install
36
36
 
37
- Requires [uv](https://docs.astral.sh/uv/). Get an API key from the [NIPOST developer dashboard](https://dashboard.postcode.gov.ng); `validate_postcode` works without one.
37
+ Works with any MCP client. The server runs over stdio:
38
38
 
39
- **Claude Code**
39
+ | Setting | Value |
40
+ | --- | --- |
41
+ | Command | `uvx` |
42
+ | Arguments | `ng-postcode-mcp` |
43
+ | Environment | `NG_POSTCODE_API_KEY` (optional for `validate_postcode`) |
40
44
 
41
- ```sh
42
- claude mcp add ng-postcode -e NG_POSTCODE_API_KEY=nipost_live_... -- uvx ng-postcode-mcp
43
- ```
45
+ It needs [uv](https://docs.astral.sh/uv/) installed. Get an API key from the [NIPOST developer dashboard](https://dashboard.postcode.gov.ng).
44
46
 
45
- **Claude Desktop, Cursor and other clients** that use an `mcpServers` config:
47
+ Most clients take this entry in their MCP settings:
46
48
 
47
49
  ```json
48
50
  {
@@ -56,6 +58,15 @@ claude mcp add ng-postcode -e NG_POSTCODE_API_KEY=nipost_live_... -- uvx ng-post
56
58
  }
57
59
  ```
58
60
 
61
+ | Client | How to add it |
62
+ | --- | --- |
63
+ | Claude Code | `claude mcp add ng-postcode -e NG_POSTCODE_API_KEY=nipost_live_... -- uvx ng-postcode-mcp` |
64
+ | Claude Desktop | The entry above, in its MCP server settings |
65
+ | Codex | `codex mcp add ng-postcode --env NG_POSTCODE_API_KEY=nipost_live_... -- uvx ng-postcode-mcp` |
66
+ | Cursor | The entry above, in `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global) |
67
+ | VS Code | The same server object in `.vscode/mcp.json`, under a top-level `"servers"` key instead of `"mcpServers"` |
68
+ | Others | Any client that launches stdio servers: use the command, arguments and environment above |
69
+
59
70
  ## Configuration
60
71
 
61
72
  | Variable | Default | Purpose |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ng-postcode-mcp"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "MCP server for Nigeria's NIPOST digital postcode (NDAPS): validate, look up, reverse-geocode and resolve addresses to postcodes."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -26,7 +26,7 @@ classifiers = [
26
26
  "Topic :: Scientific/Engineering :: GIS",
27
27
  "Typing :: Typed",
28
28
  ]
29
- dependencies = ["mcp>=2.3,<3", "ng-address-resolver>=0.1,<0.2", "ng-postcode[client]>=0.1,<0.2"]
29
+ dependencies = ["mcp>=2.3,<3", "ng-address-resolver>=0.1.1,<0.2", "ng-postcode[client]>=0.2,<0.3"]
30
30
 
31
31
  [project.scripts]
32
32
  ng-postcode-mcp = "ng_postcode_mcp.server:main"
@@ -3,7 +3,7 @@
3
3
  "name": "io.github.Adeniyikayodee/ng-postcode",
4
4
  "title": "Nigeria Postcode",
5
5
  "description": "Validate, look up and resolve addresses to Nigeria's NIPOST digital postcodes (NDAPS).",
6
- "version": "0.2.0",
6
+ "version": "0.2.2",
7
7
  "repository": {
8
8
  "url": "https://github.com/Adeniyikayodee/ng-postcode",
9
9
  "source": "github",
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "registryType": "pypi",
15
15
  "identifier": "ng-postcode-mcp",
16
- "version": "0.2.0",
16
+ "version": "0.2.2",
17
17
  "runtimeHint": "uvx",
18
18
  "transport": {
19
19
  "type": "stdio"
@@ -62,6 +62,8 @@ HINTS = {
62
62
  "auth_required": f"Set NG_POSTCODE_API_KEY to a key from {KEY_URL}.",
63
63
  "invalid_api_key": f"NG_POSTCODE_API_KEY is invalid or revoked; create a new key at {KEY_URL}.",
64
64
  "insufficient_credits": "The NIPOST account is out of credits; top up or use level 1.",
65
+ "level_not_granted": "The key is not granted this lookup level; use a lower level or "
66
+ f"request more access at {KEY_URL}.",
65
67
  }
66
68
  STATUS_HINTS = {
67
69
  403: "The key lacks the scope or access level for this request.",
@@ -136,6 +138,10 @@ class Address(FromLibrary):
136
138
  class PostcodeDetails(FromLibrary):
137
139
  postcode: str
138
140
  valid: bool = Field(description="Whether the code is assigned to a building.")
141
+ status: str | None = Field(
142
+ default=None, description="valid, not_found, or invalid for a malformed code."
143
+ )
144
+ verified: bool | None = None
139
145
  administrative_address: Address | None = Field(description="Level 2 and up.")
140
146
  recent_house_address: str | None = Field(description="Level 2 and up. Personal data.")
141
147
  building_use_status: str | None = Field(description="Level 3 and up, e.g. residential.")
@@ -144,8 +150,10 @@ class PostcodeDetails(FromLibrary):
144
150
 
145
151
 
146
152
  class Completion(FromLibrary):
147
- code: str
148
- label: str
153
+ code: str = Field(description="The value of the next segment, e.g. A03, not a full prefix.")
154
+ label: str | None = Field(
155
+ default=None, description="A name for the code, when NIPOST sends one."
156
+ )
149
157
 
150
158
 
151
159
  class Completions(BaseModel):
@@ -174,6 +182,7 @@ class Location(FromLibrary):
174
182
  state: str | None
175
183
  message: str | None
176
184
  radius_m: float | None = Field(description="The radius the API actually applied.")
185
+ depth: str | None = Field(default=None, description="How deep the match goes, e.g. unit.")
177
186
 
178
187
 
179
188
  @dataclass(frozen=True, slots=True)
@@ -264,10 +273,14 @@ def create_server(
264
273
  structured_output=True,
265
274
  )
266
275
  async def autocomplete_postcode(
267
- partial: Annotated[str, Field(description="The start of a code, e.g. 'EK 01 A'.")],
276
+ partial: Annotated[
277
+ str, Field(min_length=1, description="The start of a code, e.g. 'EK 01 A'.")
278
+ ],
268
279
  ctx: Context[State, Any],
269
280
  ) -> Completions:
270
281
  """Suggest completions for the next segment of a partly typed postcode."""
282
+ if not partial.strip():
283
+ raise ToolError("Give at least the first characters of a postcode.")
271
284
  result = await api(ctx).send(autocomplete(partial))
272
285
  return completions(unwrap(result))
273
286
 
@@ -9,8 +9,10 @@ from typing import Any
9
9
  import httpx
10
10
  import pytest
11
11
  from mcp import Client
12
+ from mcp.server.mcpserver.exceptions import ToolError
12
13
  from mcp.types import CallToolResult, TextContent
13
14
  from ng_postcode import api
15
+ from ng_postcode.api import ApiError
14
16
  from pydantic import BaseModel
15
17
 
16
18
  from ng_postcode_mcp import Settings, create_server, settings_from_env
@@ -20,6 +22,7 @@ from ng_postcode_mcp.server import (
20
22
  Location,
21
23
  NearestBuilding,
22
24
  PostcodeDetails,
25
+ unwrap,
23
26
  )
24
27
 
25
28
  Handler = Callable[[httpx.Request], httpx.Response]
@@ -208,3 +211,17 @@ def test_settings_from_env() -> None:
208
211
  assert settings_from_env({"NG_POSTCODE_MAX_LEVEL": "9"}) == (
209
212
  "NG_POSTCODE_MAX_LEVEL must be 1 to 5, got '9'"
210
213
  )
214
+
215
+
216
+ @pytest.mark.anyio
217
+ async def test_a_blank_autocomplete_never_reaches_the_api() -> None:
218
+ seen: list[httpx.Request] = []
219
+ result = await call("autocomplete_postcode", {"partial": " "}, seen=seen)
220
+ assert result.is_error
221
+ assert seen == []
222
+
223
+
224
+ def test_a_level_the_key_lacks_gets_a_specific_hint() -> None:
225
+ error = ApiError(403, "level_not_granted", "this key is granted up to lookup level 1")
226
+ with pytest.raises(ToolError, match="request more access"):
227
+ unwrap(error)
File without changes