ng-postcode-mcp 0.1.0__tar.gz → 0.2.1__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.
- ng_postcode_mcp-0.2.1/PKG-INFO +119 -0
- ng_postcode_mcp-0.2.1/README.md +91 -0
- {ng_postcode_mcp-0.1.0 → ng_postcode_mcp-0.2.1}/pyproject.toml +5 -4
- {ng_postcode_mcp-0.1.0 → ng_postcode_mcp-0.2.1}/server.json +18 -4
- {ng_postcode_mcp-0.1.0 → ng_postcode_mcp-0.2.1}/src/ng_postcode_mcp/server.py +122 -18
- ng_postcode_mcp-0.2.1/tests/test_resolve_tool.py +144 -0
- {ng_postcode_mcp-0.1.0 → ng_postcode_mcp-0.2.1}/tests/test_tools.py +2 -1
- ng_postcode_mcp-0.1.0/PKG-INFO +0 -87
- ng_postcode_mcp-0.1.0/README.md +0 -60
- {ng_postcode_mcp-0.1.0 → ng_postcode_mcp-0.2.1}/.gitignore +0 -0
- {ng_postcode_mcp-0.1.0 → ng_postcode_mcp-0.2.1}/LICENSE +0 -0
- {ng_postcode_mcp-0.1.0 → ng_postcode_mcp-0.2.1}/src/ng_postcode_mcp/__init__.py +0 -0
- {ng_postcode_mcp-0.1.0 → ng_postcode_mcp-0.2.1}/src/ng_postcode_mcp/__main__.py +0 -0
- {ng_postcode_mcp-0.1.0 → ng_postcode_mcp-0.2.1}/src/ng_postcode_mcp/py.typed +0 -0
- {ng_postcode_mcp-0.1.0 → ng_postcode_mcp-0.2.1}/tests/test_metadata.py +0 -0
- {ng_postcode_mcp-0.1.0 → ng_postcode_mcp-0.2.1}/tests/test_stdio.py +0 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ng-postcode-mcp
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: MCP server for Nigeria's NIPOST digital postcode (NDAPS): validate, look up, reverse-geocode and resolve addresses to postcodes.
|
|
5
|
+
Project-URL: Repository, https://github.com/Adeniyikayodee/ng-postcode
|
|
6
|
+
Project-URL: Issues, https://github.com/Adeniyikayodee/ng-postcode/issues
|
|
7
|
+
Author: Kayode Adeniyi
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: ai-agents,geocoding,mcp,mcp-server,model-context-protocol,ndaps,nigeria,nipost,postcode
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
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
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# ng-postcode-mcp
|
|
30
|
+
|
|
31
|
+
MCP server for Nigeria's National Digital Alphanumeric Postcode System (NDAPS), the building-level postcode NIPOST launched in October 2026. It lets AI assistants validate postcodes offline, look them up, autocomplete them and find them by location through the [postcode.gov.ng](https://docs.postcode.gov.ng) API, and resolve described addresses to postcodes.
|
|
32
|
+
|
|
33
|
+
Built on the [`ng-postcode`](https://pypi.org/project/ng-postcode/) and [`ng-address-resolver`](https://pypi.org/project/ng-address-resolver/) libraries.
|
|
34
|
+
|
|
35
|
+
<!-- mcp-name: io.github.Adeniyikayodee/ng-postcode -->
|
|
36
|
+
|
|
37
|
+
## Tools
|
|
38
|
+
|
|
39
|
+
| Tool | What it does | Needs a key | Cost |
|
|
40
|
+
| --- | --- | --- | --- |
|
|
41
|
+
| `validate_postcode` | Checks structure offline; returns canonical forms, segments and a suggested fix for look-alike characters | No | Free |
|
|
42
|
+
| `lookup_postcode` | Confirms a code is assigned; level 2 adds the address, level 3 building use | Yes | Level 1 free, 2+ uses credits |
|
|
43
|
+
| `autocomplete_postcode` | Suggests the next segment of a partly typed code | Yes | Free tier |
|
|
44
|
+
| `find_postcode_at_location` | Returns the postcode of the nearest building to a coordinate | Yes | Free tier |
|
|
45
|
+
| `resolve_address` | Turns a described address ("back of Fabian Hotel, off NTA Road") or a location pin into a postcode, only as precisely as the evidence allows | Yes, plus a geocoder for text | Free tier |
|
|
46
|
+
|
|
47
|
+
All tools are read-only. Errors come back as messages the model can act on, such as a missing key or an exhausted credit balance.
|
|
48
|
+
|
|
49
|
+
### How `resolve_address` answers
|
|
50
|
+
|
|
51
|
+
The assistant reads the address and passes its landmarks and map searches to the tool; the server makes no model calls of its own. The answer is never more precise than its evidence:
|
|
52
|
+
|
|
53
|
+
| Evidence | Answer |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| A postcode written in the address, or a location pin on a building | Building code |
|
|
56
|
+
| A landmark the address *is* | Building code, medium confidence |
|
|
57
|
+
| A building near a landmark ("behind", "opposite") | Area code, plus a question for the user |
|
|
58
|
+
| A street only | District code, low confidence |
|
|
59
|
+
| A town only, or nothing found | No code, plus a question |
|
|
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.
|
|
62
|
+
|
|
63
|
+
## Install
|
|
64
|
+
|
|
65
|
+
Works with any MCP client. The server runs over stdio:
|
|
66
|
+
|
|
67
|
+
| Setting | Value |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| Command | `uvx` |
|
|
70
|
+
| Arguments | `ng-postcode-mcp` |
|
|
71
|
+
| Environment | `NG_POSTCODE_API_KEY` (optional for `validate_postcode`) |
|
|
72
|
+
|
|
73
|
+
It needs [uv](https://docs.astral.sh/uv/) installed. Get an API key from the [NIPOST developer dashboard](https://dashboard.postcode.gov.ng).
|
|
74
|
+
|
|
75
|
+
Most clients take this entry in their MCP settings:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"mcpServers": {
|
|
80
|
+
"ng-postcode": {
|
|
81
|
+
"command": "uvx",
|
|
82
|
+
"args": ["ng-postcode-mcp"],
|
|
83
|
+
"env": { "NG_POSTCODE_API_KEY": "nipost_live_..." }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
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
|
+
|
|
98
|
+
## Configuration
|
|
99
|
+
|
|
100
|
+
| Variable | Default | Purpose |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| `NG_POSTCODE_API_KEY` | none | NIPOST API key. Read from the environment only; never passed through tools. |
|
|
103
|
+
| `NG_POSTCODE_MAX_LEVEL` | `1` | Highest lookup level tools may request. Levels 2+ consume credits, so raise it deliberately. |
|
|
104
|
+
| `NG_POSTCODE_BASE_URL` | `https://api.postcode.gov.ng` | Alternative API host, such as a staging stack. |
|
|
105
|
+
| `NG_GEOCODER_URL` | none | A Nominatim server `resolve_address` uses to place described addresses. Without it, only typed postcodes and location pins resolve. |
|
|
106
|
+
| `NG_GEOCODER_CONTACT` | none | A URL or email sent in the User-Agent. Required for the public Nominatim. |
|
|
107
|
+
|
|
108
|
+
The public Nominatim at `https://nominatim.openstreetmap.org` allows light personal use only; a service whose main job is geocoding must run its own instance. Map data © OpenStreetMap contributors.
|
|
109
|
+
|
|
110
|
+
## Safety
|
|
111
|
+
|
|
112
|
+
- Lookups default to level 1, which is free. A model cannot spend credits unless you raise `NG_POSTCODE_MAX_LEVEL`.
|
|
113
|
+
- A mistyped code is never corrected and sent to the API silently. The server returns the suggestion and asks the model to confirm it with the user.
|
|
114
|
+
- Levels 2 and up return house addresses. Treat them as personal data under the Nigeria Data Protection Act.
|
|
115
|
+
- `resolve_address` sends the search strings to the geocoder you configure. With a third-party geocoder, that shares address text with it.
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# ng-postcode-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for Nigeria's National Digital Alphanumeric Postcode System (NDAPS), the building-level postcode NIPOST launched in October 2026. It lets AI assistants validate postcodes offline, look them up, autocomplete them and find them by location through the [postcode.gov.ng](https://docs.postcode.gov.ng) API, and resolve described addresses to postcodes.
|
|
4
|
+
|
|
5
|
+
Built on the [`ng-postcode`](https://pypi.org/project/ng-postcode/) and [`ng-address-resolver`](https://pypi.org/project/ng-address-resolver/) libraries.
|
|
6
|
+
|
|
7
|
+
<!-- mcp-name: io.github.Adeniyikayodee/ng-postcode -->
|
|
8
|
+
|
|
9
|
+
## Tools
|
|
10
|
+
|
|
11
|
+
| Tool | What it does | Needs a key | Cost |
|
|
12
|
+
| --- | --- | --- | --- |
|
|
13
|
+
| `validate_postcode` | Checks structure offline; returns canonical forms, segments and a suggested fix for look-alike characters | No | Free |
|
|
14
|
+
| `lookup_postcode` | Confirms a code is assigned; level 2 adds the address, level 3 building use | Yes | Level 1 free, 2+ uses credits |
|
|
15
|
+
| `autocomplete_postcode` | Suggests the next segment of a partly typed code | Yes | Free tier |
|
|
16
|
+
| `find_postcode_at_location` | Returns the postcode of the nearest building to a coordinate | Yes | Free tier |
|
|
17
|
+
| `resolve_address` | Turns a described address ("back of Fabian Hotel, off NTA Road") or a location pin into a postcode, only as precisely as the evidence allows | Yes, plus a geocoder for text | Free tier |
|
|
18
|
+
|
|
19
|
+
All tools are read-only. Errors come back as messages the model can act on, such as a missing key or an exhausted credit balance.
|
|
20
|
+
|
|
21
|
+
### How `resolve_address` answers
|
|
22
|
+
|
|
23
|
+
The assistant reads the address and passes its landmarks and map searches to the tool; the server makes no model calls of its own. The answer is never more precise than its evidence:
|
|
24
|
+
|
|
25
|
+
| Evidence | Answer |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| A postcode written in the address, or a location pin on a building | Building code |
|
|
28
|
+
| A landmark the address *is* | Building code, medium confidence |
|
|
29
|
+
| A building near a landmark ("behind", "opposite") | Area code, plus a question for the user |
|
|
30
|
+
| A street only | District code, low confidence |
|
|
31
|
+
| A town only, or nothing found | No code, plus a question |
|
|
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.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
Works with any MCP client. The server runs over stdio:
|
|
38
|
+
|
|
39
|
+
| Setting | Value |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| Command | `uvx` |
|
|
42
|
+
| Arguments | `ng-postcode-mcp` |
|
|
43
|
+
| Environment | `NG_POSTCODE_API_KEY` (optional for `validate_postcode`) |
|
|
44
|
+
|
|
45
|
+
It needs [uv](https://docs.astral.sh/uv/) installed. Get an API key from the [NIPOST developer dashboard](https://dashboard.postcode.gov.ng).
|
|
46
|
+
|
|
47
|
+
Most clients take this entry in their MCP settings:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"mcpServers": {
|
|
52
|
+
"ng-postcode": {
|
|
53
|
+
"command": "uvx",
|
|
54
|
+
"args": ["ng-postcode-mcp"],
|
|
55
|
+
"env": { "NG_POSTCODE_API_KEY": "nipost_live_..." }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
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
|
+
|
|
70
|
+
## Configuration
|
|
71
|
+
|
|
72
|
+
| Variable | Default | Purpose |
|
|
73
|
+
| --- | --- | --- |
|
|
74
|
+
| `NG_POSTCODE_API_KEY` | none | NIPOST API key. Read from the environment only; never passed through tools. |
|
|
75
|
+
| `NG_POSTCODE_MAX_LEVEL` | `1` | Highest lookup level tools may request. Levels 2+ consume credits, so raise it deliberately. |
|
|
76
|
+
| `NG_POSTCODE_BASE_URL` | `https://api.postcode.gov.ng` | Alternative API host, such as a staging stack. |
|
|
77
|
+
| `NG_GEOCODER_URL` | none | A Nominatim server `resolve_address` uses to place described addresses. Without it, only typed postcodes and location pins resolve. |
|
|
78
|
+
| `NG_GEOCODER_CONTACT` | none | A URL or email sent in the User-Agent. Required for the public Nominatim. |
|
|
79
|
+
|
|
80
|
+
The public Nominatim at `https://nominatim.openstreetmap.org` allows light personal use only; a service whose main job is geocoding must run its own instance. Map data © OpenStreetMap contributors.
|
|
81
|
+
|
|
82
|
+
## Safety
|
|
83
|
+
|
|
84
|
+
- Lookups default to level 1, which is free. A model cannot spend credits unless you raise `NG_POSTCODE_MAX_LEVEL`.
|
|
85
|
+
- A mistyped code is never corrected and sent to the API silently. The server returns the suggestion and asks the model to confirm it with the user.
|
|
86
|
+
- Levels 2 and up return house addresses. Treat them as personal data under the Nigeria Data Protection Act.
|
|
87
|
+
- `resolve_address` sends the search strings to the geocoder you configure. With a third-party geocoder, that shares address text with it.
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT
|
|
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ng-postcode-mcp"
|
|
7
|
-
version = "0.1
|
|
8
|
-
description = "MCP server for Nigeria's NIPOST digital postcode (NDAPS): validate, look up
|
|
7
|
+
version = "0.2.1"
|
|
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"
|
|
11
11
|
license = "MIT"
|
|
@@ -26,7 +26,7 @@ classifiers = [
|
|
|
26
26
|
"Topic :: Scientific/Engineering :: GIS",
|
|
27
27
|
"Typing :: Typed",
|
|
28
28
|
]
|
|
29
|
-
dependencies = ["mcp>=2.3,<3", "ng-postcode[client]>=0.1,<0.2"]
|
|
29
|
+
dependencies = ["mcp>=2.3,<3", "ng-address-resolver>=0.1,<0.2", "ng-postcode[client]>=0.1,<0.2"]
|
|
30
30
|
|
|
31
31
|
[project.scripts]
|
|
32
32
|
ng-postcode-mcp = "ng_postcode_mcp.server:main"
|
|
@@ -38,8 +38,9 @@ Issues = "https://github.com/Adeniyikayodee/ng-postcode/issues"
|
|
|
38
38
|
[dependency-groups]
|
|
39
39
|
dev = ["jsonschema>=4.20", "mypy>=1.13", "pytest>=8", "ruff>=0.8"]
|
|
40
40
|
|
|
41
|
-
# Inside the repository, build against the local
|
|
41
|
+
# Inside the repository, build against the local packages so they never drift.
|
|
42
42
|
[tool.uv.sources]
|
|
43
|
+
ng-address-resolver = { path = "../agent", editable = true }
|
|
43
44
|
ng-postcode = { path = "../python", editable = true }
|
|
44
45
|
|
|
45
46
|
[tool.hatch.build.targets.sdist]
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.Adeniyikayodee/ng-postcode",
|
|
4
4
|
"title": "Nigeria Postcode",
|
|
5
|
-
"description": "Validate, look up and
|
|
6
|
-
"version": "0.1
|
|
5
|
+
"description": "Validate, look up and resolve addresses to Nigeria's NIPOST digital postcodes (NDAPS).",
|
|
6
|
+
"version": "0.2.1",
|
|
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.1
|
|
16
|
+
"version": "0.2.1",
|
|
17
17
|
"runtimeHint": "uvx",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|
|
@@ -29,7 +29,21 @@
|
|
|
29
29
|
"name": "NG_POSTCODE_MAX_LEVEL",
|
|
30
30
|
"description": "Highest lookup level tools may request. Levels 2 and up consume NIPOST credits.",
|
|
31
31
|
"default": "1",
|
|
32
|
-
"choices": [
|
|
32
|
+
"choices": [
|
|
33
|
+
"1",
|
|
34
|
+
"2",
|
|
35
|
+
"3",
|
|
36
|
+
"4",
|
|
37
|
+
"5"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "NG_GEOCODER_URL",
|
|
42
|
+
"description": "Nominatim server used by resolve_address to place described addresses. Ideally your own instance."
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "NG_GEOCODER_CONTACT",
|
|
46
|
+
"description": "URL or email identifying you to the geocoder. Required for the public Nominatim."
|
|
33
47
|
}
|
|
34
48
|
]
|
|
35
49
|
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Validation runs offline. Lookup, autocomplete and reverse geocoding call the
|
|
4
4
|
postcode.gov.ng API with the key in NG_POSTCODE_API_KEY, which never appears in
|
|
5
|
-
tool arguments or results.
|
|
6
|
-
print to it.
|
|
5
|
+
tool arguments or results. Address resolution also searches the geocoder in
|
|
6
|
+
NG_GEOCODER_URL. stdout carries the protocol, so nothing else may print to it.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
@@ -21,6 +21,8 @@ import httpx
|
|
|
21
21
|
from mcp.server.mcpserver import Context, MCPServer
|
|
22
22
|
from mcp.server.mcpserver.exceptions import ToolError
|
|
23
23
|
from mcp.types import ToolAnnotations
|
|
24
|
+
from ng_address import Landmark, Nominatim, ParsedAddress, Resolution, Resolver
|
|
25
|
+
from ng_address.geocode import PUBLIC_NOMINATIM
|
|
24
26
|
from ng_postcode import Corrected, Postcode, parse, parse_lenient
|
|
25
27
|
from ng_postcode.api import (
|
|
26
28
|
BASE_URL,
|
|
@@ -45,6 +47,9 @@ Tools for Nigeria's 11-character building postcode, e.g. EK-01-A03-FK-01 \
|
|
|
45
47
|
- lookup_postcode level 1 only confirms a code exists. Levels 2 and up add the \
|
|
46
48
|
address and building details, consume NIPOST credits, and are capped by the \
|
|
47
49
|
server's NG_POSTCODE_MAX_LEVEL.
|
|
50
|
+
- resolve_address turns a described address into a code. It answers only as \
|
|
51
|
+
precisely as its evidence allows: pass on its level and confidence, and ask the \
|
|
52
|
+
user its question instead of guessing. A location pin is the most reliable input.
|
|
48
53
|
- Never substitute a suggested correction without confirming it with the user.
|
|
49
54
|
- Addresses returned are personal data: use them only for the user's request."""
|
|
50
55
|
|
|
@@ -69,6 +74,8 @@ class Settings:
|
|
|
69
74
|
api_key: str | None
|
|
70
75
|
max_level: int = 1
|
|
71
76
|
base_url: str = BASE_URL
|
|
77
|
+
geocoder_url: str | None = None
|
|
78
|
+
geocoder_contact: str | None = None
|
|
72
79
|
|
|
73
80
|
|
|
74
81
|
def settings_from_env(env: Mapping[str, str]) -> Settings | str:
|
|
@@ -76,10 +83,16 @@ def settings_from_env(env: Mapping[str, str]) -> Settings | str:
|
|
|
76
83
|
raw_level = env.get("NG_POSTCODE_MAX_LEVEL", "1").strip()
|
|
77
84
|
if raw_level not in {"1", "2", "3", "4", "5"}:
|
|
78
85
|
return f"NG_POSTCODE_MAX_LEVEL must be 1 to 5, got {raw_level!r}"
|
|
86
|
+
geocoder_url = env.get("NG_GEOCODER_URL", "").strip().rstrip("/") or None
|
|
87
|
+
geocoder_contact = env.get("NG_GEOCODER_CONTACT", "").strip() or None
|
|
88
|
+
if geocoder_url == PUBLIC_NOMINATIM and geocoder_contact is None:
|
|
89
|
+
return "the public Nominatim requires NG_GEOCODER_CONTACT, a URL or email identifying you"
|
|
79
90
|
return Settings(
|
|
80
91
|
api_key=env.get("NG_POSTCODE_API_KEY", "").strip() or None,
|
|
81
92
|
max_level=int(raw_level),
|
|
82
93
|
base_url=env.get("NG_POSTCODE_BASE_URL", "").strip() or BASE_URL,
|
|
94
|
+
geocoder_url=geocoder_url,
|
|
95
|
+
geocoder_contact=geocoder_contact,
|
|
83
96
|
)
|
|
84
97
|
|
|
85
98
|
|
|
@@ -163,24 +176,39 @@ class Location(FromLibrary):
|
|
|
163
176
|
radius_m: float | None = Field(description="The radius the API actually applied.")
|
|
164
177
|
|
|
165
178
|
|
|
166
|
-
|
|
179
|
+
@dataclass(frozen=True, slots=True)
|
|
180
|
+
class State:
|
|
181
|
+
"""What the tools share for the life of the server. Either part may be unconfigured."""
|
|
182
|
+
|
|
183
|
+
nipost: AsyncClient | None
|
|
184
|
+
geocoder: Nominatim | None
|
|
167
185
|
|
|
168
186
|
|
|
169
|
-
def create_server(
|
|
170
|
-
|
|
187
|
+
def create_server(
|
|
188
|
+
settings: Settings,
|
|
189
|
+
http: httpx.AsyncClient | None = None,
|
|
190
|
+
geocoder_http: httpx.AsyncClient | None = None,
|
|
191
|
+
) -> MCPServer[State]:
|
|
192
|
+
"""Build the server. Pass `http` and `geocoder_http` to route calls through your own
|
|
193
|
+
clients, as tests do."""
|
|
171
194
|
|
|
172
195
|
@asynccontextmanager
|
|
173
|
-
async def lifespan(_: MCPServer[
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
196
|
+
async def lifespan(_: MCPServer[State]) -> AsyncIterator[State]:
|
|
197
|
+
nipost = (
|
|
198
|
+
AsyncClient(settings.api_key, base_url=settings.base_url, http=http)
|
|
199
|
+
if settings.api_key
|
|
200
|
+
else None
|
|
201
|
+
)
|
|
202
|
+
geocoder = geocoder_for(settings, geocoder_http)
|
|
178
203
|
try:
|
|
179
|
-
yield
|
|
204
|
+
yield State(nipost, geocoder)
|
|
180
205
|
finally:
|
|
181
|
-
|
|
206
|
+
if nipost:
|
|
207
|
+
await nipost.aclose()
|
|
208
|
+
if geocoder:
|
|
209
|
+
await geocoder.aclose()
|
|
182
210
|
|
|
183
|
-
server: MCPServer[
|
|
211
|
+
server: MCPServer[State] = MCPServer(
|
|
184
212
|
name="ng-postcode",
|
|
185
213
|
title="Nigeria Postcode",
|
|
186
214
|
instructions=INSTRUCTIONS,
|
|
@@ -210,7 +238,7 @@ def create_server(settings: Settings, http: httpx.AsyncClient | None = None) ->
|
|
|
210
238
|
)
|
|
211
239
|
async def lookup_postcode(
|
|
212
240
|
postcode: Annotated[str, Field(description="Code in any style, e.g. EK-01-A03-FK-01.")],
|
|
213
|
-
ctx: Context[
|
|
241
|
+
ctx: Context[State, Any],
|
|
214
242
|
level: Annotated[
|
|
215
243
|
int,
|
|
216
244
|
Field(
|
|
@@ -237,7 +265,7 @@ def create_server(settings: Settings, http: httpx.AsyncClient | None = None) ->
|
|
|
237
265
|
)
|
|
238
266
|
async def autocomplete_postcode(
|
|
239
267
|
partial: Annotated[str, Field(description="The start of a code, e.g. 'EK 01 A'.")],
|
|
240
|
-
ctx: Context[
|
|
268
|
+
ctx: Context[State, Any],
|
|
241
269
|
) -> Completions:
|
|
242
270
|
"""Suggest completions for the next segment of a partly typed postcode."""
|
|
243
271
|
result = await api(ctx).send(autocomplete(partial))
|
|
@@ -251,7 +279,7 @@ def create_server(settings: Settings, http: httpx.AsyncClient | None = None) ->
|
|
|
251
279
|
async def find_postcode_at_location(
|
|
252
280
|
latitude: Annotated[float, Field(ge=-90, le=90)],
|
|
253
281
|
longitude: Annotated[float, Field(ge=-180, le=180)],
|
|
254
|
-
ctx: Context[
|
|
282
|
+
ctx: Context[State, Any],
|
|
255
283
|
max_distance_m: Annotated[
|
|
256
284
|
float | None,
|
|
257
285
|
Field(ge=0, le=250, description="Search radius in metres. Defaults to 25."),
|
|
@@ -263,9 +291,85 @@ def create_server(settings: Settings, http: httpx.AsyncClient | None = None) ->
|
|
|
263
291
|
)
|
|
264
292
|
return Location.model_validate(unwrap(result))
|
|
265
293
|
|
|
294
|
+
@server.tool(
|
|
295
|
+
title="Resolve a Nigerian address to a postcode",
|
|
296
|
+
annotations=READ_ONLY_ONLINE,
|
|
297
|
+
structured_output=True,
|
|
298
|
+
)
|
|
299
|
+
async def resolve_address(
|
|
300
|
+
address: Annotated[str, Field(description="The address exactly as the user gave it.")],
|
|
301
|
+
ctx: Context[State, Any],
|
|
302
|
+
landmarks: Annotated[
|
|
303
|
+
list[Landmark] | None,
|
|
304
|
+
Field(
|
|
305
|
+
description="Landmarks named in the address, each with how the address relates "
|
|
306
|
+
"to it. Use 'at' only when the address is the landmark itself."
|
|
307
|
+
),
|
|
308
|
+
] = None,
|
|
309
|
+
geocode_queries: Annotated[
|
|
310
|
+
list[str] | None,
|
|
311
|
+
Field(
|
|
312
|
+
max_length=3,
|
|
313
|
+
description="Up to three map search strings you derive from the address: named "
|
|
314
|
+
"places and streets with the town and state, most specific first, without "
|
|
315
|
+
"directional words like 'back of'.",
|
|
316
|
+
),
|
|
317
|
+
] = None,
|
|
318
|
+
latitude: Annotated[
|
|
319
|
+
float | None, Field(ge=-90, le=90, description="A location pin the user shared.")
|
|
320
|
+
] = None,
|
|
321
|
+
longitude: Annotated[float | None, Field(ge=-180, le=180)] = None,
|
|
322
|
+
) -> Resolution:
|
|
323
|
+
"""Turn a described Nigerian address into a postcode, only as precisely as the
|
|
324
|
+
evidence allows.
|
|
325
|
+
|
|
326
|
+
A full building code comes only from a postcode written in the address, a location
|
|
327
|
+
pin, or a landmark that is the address itself. Otherwise the result is an area or
|
|
328
|
+
district prefix with a `question` for the user. Read the address yourself and fill
|
|
329
|
+
`landmarks` and `geocode_queries`; the address text is sent to the configured
|
|
330
|
+
geocoder.
|
|
331
|
+
"""
|
|
332
|
+
if (latitude is None) != (longitude is None):
|
|
333
|
+
raise ToolError("Give both latitude and longitude, or neither.")
|
|
334
|
+
location = (
|
|
335
|
+
Coordinate(lat=latitude, lng=longitude)
|
|
336
|
+
if latitude is not None and longitude is not None
|
|
337
|
+
else None
|
|
338
|
+
)
|
|
339
|
+
state = ctx.request_context.lifespan_context
|
|
340
|
+
resolver = Resolver(nipost=state.nipost, geocoder=state.geocoder)
|
|
341
|
+
return await resolver.resolve(address, location, reading(landmarks, geocode_queries))
|
|
342
|
+
|
|
266
343
|
return server
|
|
267
344
|
|
|
268
345
|
|
|
346
|
+
def reading(landmarks: list[Landmark] | None, queries: list[str] | None) -> ParsedAddress | None:
|
|
347
|
+
"""The host model's reading of the address, in the resolver's terms."""
|
|
348
|
+
if not landmarks and not queries:
|
|
349
|
+
return None
|
|
350
|
+
return ParsedAddress(
|
|
351
|
+
house_number=None,
|
|
352
|
+
street=None,
|
|
353
|
+
landmarks=landmarks or [],
|
|
354
|
+
locality=None,
|
|
355
|
+
lga=None,
|
|
356
|
+
state=None,
|
|
357
|
+
geocode_queries=queries or [],
|
|
358
|
+
question=None,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def geocoder_for(settings: Settings, http: httpx.AsyncClient | None) -> Nominatim | None:
|
|
363
|
+
if settings.geocoder_url is None:
|
|
364
|
+
return None
|
|
365
|
+
contact = settings.geocoder_contact or "unknown"
|
|
366
|
+
return Nominatim(
|
|
367
|
+
settings.geocoder_url,
|
|
368
|
+
f"ng-postcode-mcp/{version('ng-postcode-mcp')} ({contact})",
|
|
369
|
+
http=http,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
|
|
269
373
|
def completions(found: Autocomplete) -> Completions:
|
|
270
374
|
return Completions(
|
|
271
375
|
segment=None if found.segment is None else found.segment.value,
|
|
@@ -319,8 +423,8 @@ def checked(text: str) -> Postcode:
|
|
|
319
423
|
raise ToolError(f"{text!r} is not a valid postcode: {error}.{maybe}")
|
|
320
424
|
|
|
321
425
|
|
|
322
|
-
def api(ctx: Context[
|
|
323
|
-
client = ctx.request_context.lifespan_context
|
|
426
|
+
def api(ctx: Context[State, Any]) -> AsyncClient:
|
|
427
|
+
client = ctx.request_context.lifespan_context.nipost
|
|
324
428
|
if client is None:
|
|
325
429
|
raise ToolError(f"This tool needs NG_POSTCODE_API_KEY. {HINTS['auth_required']}")
|
|
326
430
|
return client
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""resolve_address through a real MCP client, with NIPOST and the geocoder mocked."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import pytest
|
|
9
|
+
from mcp import Client
|
|
10
|
+
from mcp.types import CallToolResult
|
|
11
|
+
|
|
12
|
+
from ng_postcode_mcp import Settings, create_server, settings_from_env
|
|
13
|
+
|
|
14
|
+
PLACES = {"Fabian Hotel, Ado Ekiti, Ekiti": 30, "NTA Road, Ado Ekiti, Ekiti": 26}
|
|
15
|
+
BEHIND_FABIAN = {
|
|
16
|
+
"address": "back of Fabian Hotel, off NTA Road, Ado Ekiti",
|
|
17
|
+
"landmarks": [{"name": "Fabian Hotel", "relation": "behind"}],
|
|
18
|
+
"geocode_queries": ["Fabian Hotel, Ado Ekiti, Ekiti", "NTA Road, Ado Ekiti, Ekiti"],
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def anyio_backend() -> str:
|
|
24
|
+
return "asyncio"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def nipost(request: httpx.Request) -> httpx.Response:
|
|
28
|
+
if request.url.path == "/v1/lookup":
|
|
29
|
+
return httpx.Response(200, json={"data": {"postcode": "EK-01-A03-FK-01", "valid": True}})
|
|
30
|
+
unit = {"postcode": "EK-01-A03-FK-01", "display": "EK 01 A03 FK 01", "distance_m": 8.0}
|
|
31
|
+
data = {"found": True, "unit": unit, "area": "EK-01-A03-FK", "district": "EK-01-A03"}
|
|
32
|
+
return httpx.Response(200, json={"data": data})
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def call(
|
|
36
|
+
arguments: dict[str, Any],
|
|
37
|
+
*,
|
|
38
|
+
geocoder: bool = True,
|
|
39
|
+
searched: list[httpx.Request] | None = None,
|
|
40
|
+
) -> CallToolResult:
|
|
41
|
+
def geocode(request: httpx.Request) -> httpx.Response:
|
|
42
|
+
if searched is not None:
|
|
43
|
+
searched.append(request)
|
|
44
|
+
query = request.url.params["q"]
|
|
45
|
+
rank = PLACES.get(query)
|
|
46
|
+
hit = {"lat": "7.62", "lon": "5.19", "place_rank": rank, "display_name": query}
|
|
47
|
+
return httpx.Response(200, json=[hit] if rank else [])
|
|
48
|
+
|
|
49
|
+
settings = Settings(
|
|
50
|
+
api_key="good",
|
|
51
|
+
geocoder_url="https://geo.example" if geocoder else None,
|
|
52
|
+
geocoder_contact="ops@example.com",
|
|
53
|
+
)
|
|
54
|
+
nipost_http = httpx.AsyncClient(transport=httpx.MockTransport(nipost))
|
|
55
|
+
geocoder_http = httpx.AsyncClient(transport=httpx.MockTransport(geocode))
|
|
56
|
+
server = create_server(settings, http=nipost_http, geocoder_http=geocoder_http)
|
|
57
|
+
async with Client(server) as client:
|
|
58
|
+
result = await client.call_tool("resolve_address", arguments)
|
|
59
|
+
await nipost_http.aclose()
|
|
60
|
+
await geocoder_http.aclose()
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pytest.mark.anyio
|
|
65
|
+
async def test_the_host_models_reading_drives_the_answer() -> None:
|
|
66
|
+
searched: list[httpx.Request] = []
|
|
67
|
+
result = await call(BEHIND_FABIAN, searched=searched)
|
|
68
|
+
assert not result.is_error
|
|
69
|
+
answer = result.structured_content
|
|
70
|
+
assert (answer["status"], answer["level"], answer["code"]) == (
|
|
71
|
+
"partial",
|
|
72
|
+
"area",
|
|
73
|
+
"EK-01-A03-FK",
|
|
74
|
+
)
|
|
75
|
+
assert "behind Fabian Hotel" in answer["question"]
|
|
76
|
+
assert [r.url.params["q"] for r in searched] == ["Fabian Hotel, Ado Ekiti, Ekiti"]
|
|
77
|
+
assert searched[0].headers["User-Agent"].startswith("ng-postcode-mcp/")
|
|
78
|
+
assert "ops@example.com" in searched[0].headers["User-Agent"]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@pytest.mark.anyio
|
|
82
|
+
async def test_a_typed_postcode_needs_no_geocoder() -> None:
|
|
83
|
+
searched: list[httpx.Request] = []
|
|
84
|
+
result = await call({"address": "deliver to ek01a03fk01"}, searched=searched)
|
|
85
|
+
answer = result.structured_content
|
|
86
|
+
assert (answer["status"], answer["code"], answer["confidence"]) == (
|
|
87
|
+
"resolved",
|
|
88
|
+
"EK-01-A03-FK-01",
|
|
89
|
+
"high",
|
|
90
|
+
)
|
|
91
|
+
assert searched == []
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@pytest.mark.anyio
|
|
95
|
+
async def test_a_location_pin_gives_the_building() -> None:
|
|
96
|
+
result = await call({"address": "my house", "latitude": 7.62, "longitude": 5.19})
|
|
97
|
+
answer = result.structured_content
|
|
98
|
+
assert (answer["level"], answer["method"], answer["code"]) == (
|
|
99
|
+
"building",
|
|
100
|
+
"location",
|
|
101
|
+
"EK-01-A03-FK-01",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@pytest.mark.anyio
|
|
106
|
+
async def test_without_a_geocoder_it_explains_and_asks() -> None:
|
|
107
|
+
result = await call(BEHIND_FABIAN, geocoder=False)
|
|
108
|
+
assert not result.is_error
|
|
109
|
+
answer = result.structured_content
|
|
110
|
+
assert (answer["status"], answer["code"]) == ("unresolved", None)
|
|
111
|
+
assert any("NG_GEOCODER_URL" in line for line in answer["evidence"])
|
|
112
|
+
assert answer["question"]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@pytest.mark.anyio
|
|
116
|
+
async def test_half_a_coordinate_is_an_error_the_model_can_fix() -> None:
|
|
117
|
+
result = await call({"address": "my house", "latitude": 7.62})
|
|
118
|
+
assert result.is_error
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@pytest.mark.anyio
|
|
122
|
+
async def test_schemas_guide_the_host_model() -> None:
|
|
123
|
+
async with Client(create_server(Settings(api_key=None))) as client:
|
|
124
|
+
tools = {tool.name: tool for tool in (await client.list_tools()).tools}
|
|
125
|
+
tool = tools["resolve_address"]
|
|
126
|
+
assert tool.input_schema["required"] == ["address"]
|
|
127
|
+
relations = tool.input_schema["$defs"]["Landmark"]["properties"]["relation"]["enum"]
|
|
128
|
+
assert {"at", "behind", "opposite"} <= set(relations)
|
|
129
|
+
assert tool.input_schema["properties"]["geocode_queries"]["anyOf"][0]["maxItems"] == 3
|
|
130
|
+
assert tool.output_schema is not None
|
|
131
|
+
assert {"status", "code", "level", "confidence", "question", "evidence"} <= set(
|
|
132
|
+
tool.output_schema["properties"]
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_geocoder_settings() -> None:
|
|
137
|
+
public = {"NG_GEOCODER_URL": "https://nominatim.openstreetmap.org/"}
|
|
138
|
+
assert settings_from_env(public) == (
|
|
139
|
+
"the public Nominatim requires NG_GEOCODER_CONTACT, a URL or email identifying you"
|
|
140
|
+
)
|
|
141
|
+
configured = settings_from_env(public | {"NG_GEOCODER_CONTACT": "https://example.com"})
|
|
142
|
+
assert isinstance(configured, Settings)
|
|
143
|
+
assert configured.geocoder_url == "https://nominatim.openstreetmap.org"
|
|
144
|
+
assert settings_from_env({}) == Settings(api_key=None)
|
|
@@ -86,7 +86,7 @@ def text(result: CallToolResult) -> str:
|
|
|
86
86
|
|
|
87
87
|
|
|
88
88
|
@pytest.mark.anyio
|
|
89
|
-
async def
|
|
89
|
+
async def test_lists_five_read_only_tools_with_schemas() -> None:
|
|
90
90
|
async with Client(create_server(Settings(api_key=None))) as client:
|
|
91
91
|
tools = {tool.name: tool for tool in (await client.list_tools()).tools}
|
|
92
92
|
assert set(tools) == {
|
|
@@ -94,6 +94,7 @@ async def test_lists_four_read_only_tools_with_schemas() -> None:
|
|
|
94
94
|
"lookup_postcode",
|
|
95
95
|
"autocomplete_postcode",
|
|
96
96
|
"find_postcode_at_location",
|
|
97
|
+
"resolve_address",
|
|
97
98
|
}
|
|
98
99
|
for tool in tools.values():
|
|
99
100
|
assert tool.annotations is not None
|
ng_postcode_mcp-0.1.0/PKG-INFO
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: ng-postcode-mcp
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: MCP server for Nigeria's NIPOST digital postcode (NDAPS): validate, look up and reverse-geocode postcodes.
|
|
5
|
-
Project-URL: Repository, https://github.com/Adeniyikayodee/ng-postcode
|
|
6
|
-
Project-URL: Issues, https://github.com/Adeniyikayodee/ng-postcode/issues
|
|
7
|
-
Author: Kayode Adeniyi
|
|
8
|
-
License-Expression: MIT
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Keywords: ai-agents,geocoding,mcp,mcp-server,model-context-protocol,ndaps,nigeria,nipost,postcode
|
|
11
|
-
Classifier: Development Status :: 3 - Alpha
|
|
12
|
-
Classifier: Intended Audience :: Developers
|
|
13
|
-
Classifier: Operating System :: OS Independent
|
|
14
|
-
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
-
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
22
|
-
Classifier: Typing :: Typed
|
|
23
|
-
Requires-Python: >=3.10
|
|
24
|
-
Requires-Dist: mcp<3,>=2.3
|
|
25
|
-
Requires-Dist: ng-postcode[client]<0.2,>=0.1
|
|
26
|
-
Description-Content-Type: text/markdown
|
|
27
|
-
|
|
28
|
-
# ng-postcode-mcp
|
|
29
|
-
|
|
30
|
-
MCP server for Nigeria's National Digital Alphanumeric Postcode System (NDAPS), the building-level postcode NIPOST launched in October 2026. It lets AI assistants validate postcodes offline and look them up, autocomplete them and find them by location through the [postcode.gov.ng](https://docs.postcode.gov.ng) API.
|
|
31
|
-
|
|
32
|
-
Built on the [`ng-postcode`](https://pypi.org/project/ng-postcode/) library.
|
|
33
|
-
|
|
34
|
-
<!-- mcp-name: io.github.Adeniyikayodee/ng-postcode -->
|
|
35
|
-
|
|
36
|
-
## Tools
|
|
37
|
-
|
|
38
|
-
| Tool | What it does | Needs a key | Cost |
|
|
39
|
-
| --- | --- | --- | --- |
|
|
40
|
-
| `validate_postcode` | Checks structure offline; returns canonical forms, segments and a suggested fix for look-alike characters | No | Free |
|
|
41
|
-
| `lookup_postcode` | Confirms a code is assigned; level 2 adds the address, level 3 building use | Yes | Level 1 free, 2+ uses credits |
|
|
42
|
-
| `autocomplete_postcode` | Suggests the next segment of a partly typed code | Yes | Free tier |
|
|
43
|
-
| `find_postcode_at_location` | Returns the postcode of the nearest building to a coordinate | Yes | Free tier |
|
|
44
|
-
|
|
45
|
-
All tools are read-only. Errors come back as messages the model can act on, such as a missing key or an exhausted credit balance.
|
|
46
|
-
|
|
47
|
-
## Install
|
|
48
|
-
|
|
49
|
-
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.
|
|
50
|
-
|
|
51
|
-
**Claude Code**
|
|
52
|
-
|
|
53
|
-
```sh
|
|
54
|
-
claude mcp add ng-postcode -e NG_POSTCODE_API_KEY=nipost_live_... -- uvx ng-postcode-mcp
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
**Claude Desktop, Cursor and other clients** that use an `mcpServers` config:
|
|
58
|
-
|
|
59
|
-
```json
|
|
60
|
-
{
|
|
61
|
-
"mcpServers": {
|
|
62
|
-
"ng-postcode": {
|
|
63
|
-
"command": "uvx",
|
|
64
|
-
"args": ["ng-postcode-mcp"],
|
|
65
|
-
"env": { "NG_POSTCODE_API_KEY": "nipost_live_..." }
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
## Configuration
|
|
72
|
-
|
|
73
|
-
| Variable | Default | Purpose |
|
|
74
|
-
| --- | --- | --- |
|
|
75
|
-
| `NG_POSTCODE_API_KEY` | none | NIPOST API key. Read from the environment only; never passed through tools. |
|
|
76
|
-
| `NG_POSTCODE_MAX_LEVEL` | `1` | Highest lookup level tools may request. Levels 2+ consume credits, so raise it deliberately. |
|
|
77
|
-
| `NG_POSTCODE_BASE_URL` | `https://api.postcode.gov.ng` | Alternative API host, such as a staging stack. |
|
|
78
|
-
|
|
79
|
-
## Safety
|
|
80
|
-
|
|
81
|
-
- Lookups default to level 1, which is free. A model cannot spend credits unless you raise `NG_POSTCODE_MAX_LEVEL`.
|
|
82
|
-
- A mistyped code is never corrected and sent to the API silently. The server returns the suggestion and asks the model to confirm it with the user.
|
|
83
|
-
- Levels 2 and up return house addresses. Treat them as personal data under the Nigeria Data Protection Act.
|
|
84
|
-
|
|
85
|
-
## License
|
|
86
|
-
|
|
87
|
-
MIT
|
ng_postcode_mcp-0.1.0/README.md
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
# ng-postcode-mcp
|
|
2
|
-
|
|
3
|
-
MCP server for Nigeria's National Digital Alphanumeric Postcode System (NDAPS), the building-level postcode NIPOST launched in October 2026. It lets AI assistants validate postcodes offline and look them up, autocomplete them and find them by location through the [postcode.gov.ng](https://docs.postcode.gov.ng) API.
|
|
4
|
-
|
|
5
|
-
Built on the [`ng-postcode`](https://pypi.org/project/ng-postcode/) library.
|
|
6
|
-
|
|
7
|
-
<!-- mcp-name: io.github.Adeniyikayodee/ng-postcode -->
|
|
8
|
-
|
|
9
|
-
## Tools
|
|
10
|
-
|
|
11
|
-
| Tool | What it does | Needs a key | Cost |
|
|
12
|
-
| --- | --- | --- | --- |
|
|
13
|
-
| `validate_postcode` | Checks structure offline; returns canonical forms, segments and a suggested fix for look-alike characters | No | Free |
|
|
14
|
-
| `lookup_postcode` | Confirms a code is assigned; level 2 adds the address, level 3 building use | Yes | Level 1 free, 2+ uses credits |
|
|
15
|
-
| `autocomplete_postcode` | Suggests the next segment of a partly typed code | Yes | Free tier |
|
|
16
|
-
| `find_postcode_at_location` | Returns the postcode of the nearest building to a coordinate | Yes | Free tier |
|
|
17
|
-
|
|
18
|
-
All tools are read-only. Errors come back as messages the model can act on, such as a missing key or an exhausted credit balance.
|
|
19
|
-
|
|
20
|
-
## Install
|
|
21
|
-
|
|
22
|
-
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.
|
|
23
|
-
|
|
24
|
-
**Claude Code**
|
|
25
|
-
|
|
26
|
-
```sh
|
|
27
|
-
claude mcp add ng-postcode -e NG_POSTCODE_API_KEY=nipost_live_... -- uvx ng-postcode-mcp
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
**Claude Desktop, Cursor and other clients** that use an `mcpServers` config:
|
|
31
|
-
|
|
32
|
-
```json
|
|
33
|
-
{
|
|
34
|
-
"mcpServers": {
|
|
35
|
-
"ng-postcode": {
|
|
36
|
-
"command": "uvx",
|
|
37
|
-
"args": ["ng-postcode-mcp"],
|
|
38
|
-
"env": { "NG_POSTCODE_API_KEY": "nipost_live_..." }
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
## Configuration
|
|
45
|
-
|
|
46
|
-
| Variable | Default | Purpose |
|
|
47
|
-
| --- | --- | --- |
|
|
48
|
-
| `NG_POSTCODE_API_KEY` | none | NIPOST API key. Read from the environment only; never passed through tools. |
|
|
49
|
-
| `NG_POSTCODE_MAX_LEVEL` | `1` | Highest lookup level tools may request. Levels 2+ consume credits, so raise it deliberately. |
|
|
50
|
-
| `NG_POSTCODE_BASE_URL` | `https://api.postcode.gov.ng` | Alternative API host, such as a staging stack. |
|
|
51
|
-
|
|
52
|
-
## Safety
|
|
53
|
-
|
|
54
|
-
- Lookups default to level 1, which is free. A model cannot spend credits unless you raise `NG_POSTCODE_MAX_LEVEL`.
|
|
55
|
-
- A mistyped code is never corrected and sent to the API silently. The server returns the suggestion and asks the model to confirm it with the user.
|
|
56
|
-
- Levels 2 and up return house addresses. Treat them as personal data under the Nigeria Data Protection Act.
|
|
57
|
-
|
|
58
|
-
## License
|
|
59
|
-
|
|
60
|
-
MIT
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|