ng-postcode-mcp 0.2.4__tar.gz → 0.3.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.
- {ng_postcode_mcp-0.2.4 → ng_postcode_mcp-0.3.0}/PKG-INFO +12 -1
- {ng_postcode_mcp-0.2.4 → ng_postcode_mcp-0.3.0}/README.md +11 -0
- {ng_postcode_mcp-0.2.4 → ng_postcode_mcp-0.3.0}/pyproject.toml +1 -1
- {ng_postcode_mcp-0.2.4 → ng_postcode_mcp-0.3.0}/server.json +11 -2
- {ng_postcode_mcp-0.2.4 → ng_postcode_mcp-0.3.0}/src/ng_postcode_mcp/server.py +19 -2
- ng_postcode_mcp-0.3.0/tests/test_http.py +49 -0
- {ng_postcode_mcp-0.2.4 → ng_postcode_mcp-0.3.0}/tests/test_tools.py +8 -0
- {ng_postcode_mcp-0.2.4 → ng_postcode_mcp-0.3.0}/.gitignore +0 -0
- {ng_postcode_mcp-0.2.4 → ng_postcode_mcp-0.3.0}/LICENSE +0 -0
- {ng_postcode_mcp-0.2.4 → ng_postcode_mcp-0.3.0}/src/ng_postcode_mcp/__init__.py +0 -0
- {ng_postcode_mcp-0.2.4 → ng_postcode_mcp-0.3.0}/src/ng_postcode_mcp/__main__.py +0 -0
- {ng_postcode_mcp-0.2.4 → ng_postcode_mcp-0.3.0}/src/ng_postcode_mcp/py.typed +0 -0
- {ng_postcode_mcp-0.2.4 → ng_postcode_mcp-0.3.0}/tests/test_metadata.py +0 -0
- {ng_postcode_mcp-0.2.4 → ng_postcode_mcp-0.3.0}/tests/test_resolve_tool.py +0 -0
- {ng_postcode_mcp-0.2.4 → ng_postcode_mcp-0.3.0}/tests/test_stdio.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ng-postcode-mcp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
|
@@ -104,9 +104,20 @@ Most clients take this entry in their MCP settings:
|
|
|
104
104
|
| `NG_POSTCODE_BASE_URL` | `https://api.postcode.gov.ng` | Alternative API host, such as a staging stack. |
|
|
105
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
106
|
| `NG_GEOCODER_CONTACT` | none | A URL or email sent in the User-Agent. Required for the public Nominatim. |
|
|
107
|
+
| `NG_POSTCODE_TRANSPORT` | `stdio` | `http` serves streamable HTTP at `/mcp` instead. |
|
|
108
|
+
| `NG_POSTCODE_HOST`, `NG_POSTCODE_PORT` | `127.0.0.1`, `8000` | Where the HTTP transport listens. |
|
|
107
109
|
|
|
108
110
|
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
111
|
|
|
112
|
+
## HTTP and Docker
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
NG_POSTCODE_TRANSPORT=http uvx ng-postcode-mcp # http://127.0.0.1:8000/mcp
|
|
116
|
+
docker build -t ng-postcode-mcp . && docker run --rm -i ng-postcode-mcp # from the repository root
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Over HTTP every caller uses the server's own NIPOST key and credits, and the server adds no authentication. It listens on loopback by default; put it behind your own authentication before setting `NG_POSTCODE_HOST` to a public address.
|
|
120
|
+
|
|
110
121
|
## Safety
|
|
111
122
|
|
|
112
123
|
- Lookups default to level 1, which is free. A model cannot spend credits unless you raise `NG_POSTCODE_MAX_LEVEL`.
|
|
@@ -76,9 +76,20 @@ Most clients take this entry in their MCP settings:
|
|
|
76
76
|
| `NG_POSTCODE_BASE_URL` | `https://api.postcode.gov.ng` | Alternative API host, such as a staging stack. |
|
|
77
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
78
|
| `NG_GEOCODER_CONTACT` | none | A URL or email sent in the User-Agent. Required for the public Nominatim. |
|
|
79
|
+
| `NG_POSTCODE_TRANSPORT` | `stdio` | `http` serves streamable HTTP at `/mcp` instead. |
|
|
80
|
+
| `NG_POSTCODE_HOST`, `NG_POSTCODE_PORT` | `127.0.0.1`, `8000` | Where the HTTP transport listens. |
|
|
79
81
|
|
|
80
82
|
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
83
|
|
|
84
|
+
## HTTP and Docker
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
NG_POSTCODE_TRANSPORT=http uvx ng-postcode-mcp # http://127.0.0.1:8000/mcp
|
|
88
|
+
docker build -t ng-postcode-mcp . && docker run --rm -i ng-postcode-mcp # from the repository root
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Over HTTP every caller uses the server's own NIPOST key and credits, and the server adds no authentication. It listens on loopback by default; put it behind your own authentication before setting `NG_POSTCODE_HOST` to a public address.
|
|
92
|
+
|
|
82
93
|
## Safety
|
|
83
94
|
|
|
84
95
|
- Lookups default to level 1, which is free. A model cannot spend credits unless you raise `NG_POSTCODE_MAX_LEVEL`.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ng-postcode-mcp"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
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"
|
|
@@ -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.
|
|
6
|
+
"version": "0.3.0",
|
|
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.
|
|
16
|
+
"version": "0.3.0",
|
|
17
17
|
"runtimeHint": "uvx",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|
|
@@ -44,6 +44,15 @@
|
|
|
44
44
|
{
|
|
45
45
|
"name": "NG_GEOCODER_CONTACT",
|
|
46
46
|
"description": "URL or email identifying you to the geocoder. Required for the public Nominatim."
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "NG_POSTCODE_TRANSPORT",
|
|
50
|
+
"description": "stdio (default), or http to serve streamable HTTP at /mcp on NG_POSTCODE_HOST and NG_POSTCODE_PORT.",
|
|
51
|
+
"default": "stdio",
|
|
52
|
+
"choices": [
|
|
53
|
+
"stdio",
|
|
54
|
+
"http"
|
|
55
|
+
]
|
|
47
56
|
}
|
|
48
57
|
]
|
|
49
58
|
}
|
|
@@ -3,7 +3,8 @@
|
|
|
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
5
|
tool arguments or results. Address resolution also searches the geocoder in
|
|
6
|
-
NG_GEOCODER_URL. stdout carries the protocol, so nothing else may
|
|
6
|
+
NG_GEOCODER_URL. Over stdio, stdout carries the protocol, so nothing else may
|
|
7
|
+
print to it. Over HTTP, every caller shares the server's key.
|
|
7
8
|
"""
|
|
8
9
|
|
|
9
10
|
from __future__ import annotations
|
|
@@ -79,6 +80,9 @@ class Settings:
|
|
|
79
80
|
base_url: str = BASE_URL
|
|
80
81
|
geocoder_url: str | None = None
|
|
81
82
|
geocoder_contact: str | None = None
|
|
83
|
+
transport: Literal["stdio", "http"] = "stdio"
|
|
84
|
+
host: str = "127.0.0.1"
|
|
85
|
+
port: int = 8000
|
|
82
86
|
|
|
83
87
|
|
|
84
88
|
def settings_from_env(env: Mapping[str, str]) -> Settings | str:
|
|
@@ -90,12 +94,21 @@ def settings_from_env(env: Mapping[str, str]) -> Settings | str:
|
|
|
90
94
|
geocoder_contact = env.get("NG_GEOCODER_CONTACT", "").strip() or None
|
|
91
95
|
if geocoder_url == PUBLIC_NOMINATIM and geocoder_contact is None:
|
|
92
96
|
return "the public Nominatim requires NG_GEOCODER_CONTACT, a URL or email identifying you"
|
|
97
|
+
transport = env.get("NG_POSTCODE_TRANSPORT", "stdio").strip().lower()
|
|
98
|
+
if transport not in ("stdio", "http"):
|
|
99
|
+
return f"NG_POSTCODE_TRANSPORT must be stdio or http, got {transport!r}"
|
|
100
|
+
raw_port = env.get("NG_POSTCODE_PORT", "8000").strip()
|
|
101
|
+
if not (raw_port.isdecimal() and 0 < int(raw_port) < 65536):
|
|
102
|
+
return f"NG_POSTCODE_PORT must be 1 to 65535, got {raw_port!r}"
|
|
93
103
|
return Settings(
|
|
94
104
|
api_key=env.get("NG_POSTCODE_API_KEY", "").strip() or None,
|
|
95
105
|
max_level=int(raw_level),
|
|
96
106
|
base_url=env.get("NG_POSTCODE_BASE_URL", "").strip() or BASE_URL,
|
|
97
107
|
geocoder_url=geocoder_url,
|
|
98
108
|
geocoder_contact=geocoder_contact,
|
|
109
|
+
transport="http" if transport == "http" else "stdio",
|
|
110
|
+
host=env.get("NG_POSTCODE_HOST", "").strip() or "127.0.0.1",
|
|
111
|
+
port=int(raw_port),
|
|
99
112
|
)
|
|
100
113
|
|
|
101
114
|
|
|
@@ -470,4 +483,8 @@ def main() -> None:
|
|
|
470
483
|
sys.exit(f"ng-postcode-mcp: {settings}")
|
|
471
484
|
# httpx logs every request URL at INFO, which would copy postcodes into client logs.
|
|
472
485
|
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
473
|
-
create_server(settings)
|
|
486
|
+
server = create_server(settings)
|
|
487
|
+
if settings.transport == "http":
|
|
488
|
+
server.run("streamable-http", host=settings.host, port=settings.port)
|
|
489
|
+
else:
|
|
490
|
+
server.run()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""The real entry point over HTTP, on a loopback port."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import socket
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
from mcp import Client
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def anyio_backend() -> str:
|
|
17
|
+
return "asyncio"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def free_port() -> int:
|
|
21
|
+
with socket.socket() as probe:
|
|
22
|
+
probe.bind(("127.0.0.1", 0))
|
|
23
|
+
return int(probe.getsockname()[1])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def wait_until_listening(port: int, seconds: float = 20.0) -> None:
|
|
27
|
+
deadline = time.monotonic() + seconds
|
|
28
|
+
while time.monotonic() < deadline:
|
|
29
|
+
with socket.socket() as probe:
|
|
30
|
+
if probe.connect_ex(("127.0.0.1", port)) == 0:
|
|
31
|
+
return
|
|
32
|
+
time.sleep(0.1)
|
|
33
|
+
raise TimeoutError(f"nothing listening on port {port}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.mark.anyio
|
|
37
|
+
async def test_serves_over_http() -> None:
|
|
38
|
+
port = free_port()
|
|
39
|
+
env = {k: v for k, v in os.environ.items() if not k.startswith("NG_POSTCODE_")}
|
|
40
|
+
env |= {"NG_POSTCODE_TRANSPORT": "http", "NG_POSTCODE_PORT": str(port)}
|
|
41
|
+
server = subprocess.Popen([sys.executable, "-m", "ng_postcode_mcp"], env=env)
|
|
42
|
+
try:
|
|
43
|
+
wait_until_listening(port)
|
|
44
|
+
async with Client(f"http://127.0.0.1:{port}/mcp") as client:
|
|
45
|
+
result = await client.call_tool("validate_postcode", {"postcode": "LA11W06TC10"})
|
|
46
|
+
finally:
|
|
47
|
+
server.terminate()
|
|
48
|
+
server.wait(timeout=10)
|
|
49
|
+
assert result.structured_content["postcode"] == "LA-11-W06-TC-10"
|
|
@@ -224,6 +224,14 @@ def test_output_models_only_use_fields_the_library_has(
|
|
|
224
224
|
assert set(model.model_fields) <= {field.name for field in dataclasses.fields(source)}
|
|
225
225
|
|
|
226
226
|
|
|
227
|
+
def test_transport_settings() -> None:
|
|
228
|
+
served = settings_from_env({"NG_POSTCODE_TRANSPORT": "HTTP", "NG_POSTCODE_PORT": "9000"})
|
|
229
|
+
assert isinstance(served, Settings)
|
|
230
|
+
assert (served.transport, served.host, served.port) == ("http", "127.0.0.1", 9000)
|
|
231
|
+
assert isinstance(settings_from_env({"NG_POSTCODE_TRANSPORT": "sse"}), str)
|
|
232
|
+
assert isinstance(settings_from_env({"NG_POSTCODE_PORT": "0"}), str)
|
|
233
|
+
|
|
234
|
+
|
|
227
235
|
def test_settings_from_env() -> None:
|
|
228
236
|
assert settings_from_env({}) == Settings(api_key=None)
|
|
229
237
|
assert settings_from_env({"NG_POSTCODE_API_KEY": " k ", "NG_POSTCODE_MAX_LEVEL": "3"}) == (
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|