ng-postcode-mcp 0.4.0__tar.gz → 0.4.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.
- {ng_postcode_mcp-0.4.0 → ng_postcode_mcp-0.4.2}/.gitignore +2 -0
- {ng_postcode_mcp-0.4.0 → ng_postcode_mcp-0.4.2}/PKG-INFO +5 -1
- {ng_postcode_mcp-0.4.0 → ng_postcode_mcp-0.4.2}/README.md +4 -0
- {ng_postcode_mcp-0.4.0 → ng_postcode_mcp-0.4.2}/pyproject.toml +1 -1
- {ng_postcode_mcp-0.4.0 → ng_postcode_mcp-0.4.2}/server.json +31 -2
- {ng_postcode_mcp-0.4.0 → ng_postcode_mcp-0.4.2}/src/ng_postcode_mcp/server.py +27 -5
- ng_postcode_mcp-0.4.2/tests/test_metadata.py +83 -0
- ng_postcode_mcp-0.4.0/tests/test_metadata.py +0 -31
- {ng_postcode_mcp-0.4.0 → ng_postcode_mcp-0.4.2}/LICENSE +0 -0
- {ng_postcode_mcp-0.4.0 → ng_postcode_mcp-0.4.2}/src/ng_postcode_mcp/__init__.py +0 -0
- {ng_postcode_mcp-0.4.0 → ng_postcode_mcp-0.4.2}/src/ng_postcode_mcp/__main__.py +0 -0
- {ng_postcode_mcp-0.4.0 → ng_postcode_mcp-0.4.2}/src/ng_postcode_mcp/py.typed +0 -0
- {ng_postcode_mcp-0.4.0 → ng_postcode_mcp-0.4.2}/tests/test_caller_key.py +0 -0
- {ng_postcode_mcp-0.4.0 → ng_postcode_mcp-0.4.2}/tests/test_http.py +0 -0
- {ng_postcode_mcp-0.4.0 → ng_postcode_mcp-0.4.2}/tests/test_reference_schema.py +0 -0
- {ng_postcode_mcp-0.4.0 → ng_postcode_mcp-0.4.2}/tests/test_resolve_tool.py +0 -0
- {ng_postcode_mcp-0.4.0 → ng_postcode_mcp-0.4.2}/tests/test_stdio.py +0 -0
- {ng_postcode_mcp-0.4.0 → ng_postcode_mcp-0.4.2}/tests/test_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ng-postcode-mcp
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -95,6 +95,10 @@ Most clients take this entry in their MCP settings:
|
|
|
95
95
|
| VS Code | The same server object in `.vscode/mcp.json`, under a top-level `"servers"` key instead of `"mcpServers"` |
|
|
96
96
|
| Others | Any client that launches stdio servers: use the command, arguments and environment above |
|
|
97
97
|
|
|
98
|
+
### As a bundle
|
|
99
|
+
|
|
100
|
+
[`bundle/`](bundle) holds an MCPB manifest for clients that install `.mcpb` files, such as Claude Desktop. It runs the published package with uv and asks for the API key in the client's own settings. Build it with `npx @anthropic-ai/mcpb pack mcp/bundle ng-postcode.mcpb` from the repository root. [`bundle-python/`](bundle-python) is the same bundle for hosts that only run Python bundles, such as Smithery; it starts the server with `uvx`, so uv must be installed. `scripts/smithery_bundle.py` builds the copy Smithery accepts, which also carries each tool's input schema.
|
|
101
|
+
|
|
98
102
|
## Configuration
|
|
99
103
|
|
|
100
104
|
| Variable | Default | Purpose |
|
|
@@ -67,6 +67,10 @@ Most clients take this entry in their MCP settings:
|
|
|
67
67
|
| VS Code | The same server object in `.vscode/mcp.json`, under a top-level `"servers"` key instead of `"mcpServers"` |
|
|
68
68
|
| Others | Any client that launches stdio servers: use the command, arguments and environment above |
|
|
69
69
|
|
|
70
|
+
### As a bundle
|
|
71
|
+
|
|
72
|
+
[`bundle/`](bundle) holds an MCPB manifest for clients that install `.mcpb` files, such as Claude Desktop. It runs the published package with uv and asks for the API key in the client's own settings. Build it with `npx @anthropic-ai/mcpb pack mcp/bundle ng-postcode.mcpb` from the repository root. [`bundle-python/`](bundle-python) is the same bundle for hosts that only run Python bundles, such as Smithery; it starts the server with `uvx`, so uv must be installed. `scripts/smithery_bundle.py` builds the copy Smithery accepts, which also carries each tool's input schema.
|
|
73
|
+
|
|
70
74
|
## Configuration
|
|
71
75
|
|
|
72
76
|
| 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.4.
|
|
7
|
+
version = "0.4.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"
|
|
@@ -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.4.
|
|
6
|
+
"version": "0.4.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.4.
|
|
16
|
+
"version": "0.4.2",
|
|
17
17
|
"runtimeHint": "uvx",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|
|
@@ -55,6 +55,35 @@
|
|
|
55
55
|
]
|
|
56
56
|
}
|
|
57
57
|
]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"registryType": "npm",
|
|
61
|
+
"identifier": "ng-postcode-mcp",
|
|
62
|
+
"version": "0.1.0",
|
|
63
|
+
"runtimeHint": "npx",
|
|
64
|
+
"transport": {
|
|
65
|
+
"type": "stdio"
|
|
66
|
+
},
|
|
67
|
+
"environmentVariables": [
|
|
68
|
+
{
|
|
69
|
+
"name": "NG_POSTCODE_API_KEY",
|
|
70
|
+
"description": "NIPOST Postcode API key from dashboard.postcode.gov.ng. Validation works without it.",
|
|
71
|
+
"isRequired": false,
|
|
72
|
+
"isSecret": true
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"name": "NG_POSTCODE_MAX_LEVEL",
|
|
76
|
+
"description": "Highest lookup level tools may request. Levels 2 and up consume NIPOST credits.",
|
|
77
|
+
"default": "1",
|
|
78
|
+
"choices": [
|
|
79
|
+
"1",
|
|
80
|
+
"2",
|
|
81
|
+
"3",
|
|
82
|
+
"4",
|
|
83
|
+
"5"
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
]
|
|
58
87
|
}
|
|
59
88
|
]
|
|
60
89
|
}
|
|
@@ -10,10 +10,11 @@ header; without one it uses the server's key, if the server has one.
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import inspect
|
|
13
14
|
import logging
|
|
14
15
|
import os
|
|
15
16
|
import sys
|
|
16
|
-
from collections.abc import AsyncIterator, Mapping
|
|
17
|
+
from collections.abc import AsyncIterator, Callable, Mapping
|
|
17
18
|
from contextlib import asynccontextmanager
|
|
18
19
|
from dataclasses import dataclass
|
|
19
20
|
from importlib.metadata import version
|
|
@@ -39,6 +40,7 @@ from ng_postcode.client import TIMEOUT, AsyncClient, TransportError
|
|
|
39
40
|
from pydantic import BaseModel, ConfigDict, Field
|
|
40
41
|
|
|
41
42
|
T = TypeVar("T")
|
|
43
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
42
44
|
|
|
43
45
|
KEY_URL = "https://dashboard.postcode.gov.ng"
|
|
44
46
|
KEY_HEADER = "x-nipost-api-key"
|
|
@@ -251,6 +253,7 @@ def create_server(
|
|
|
251
253
|
annotations=READ_ONLY_OFFLINE,
|
|
252
254
|
structured_output=True,
|
|
253
255
|
)
|
|
256
|
+
@tidy
|
|
254
257
|
def validate_postcode(
|
|
255
258
|
postcode: Annotated[str, Field(description="Code in any style, e.g. 'ek 01 a03 fk 01'.")],
|
|
256
259
|
) -> Validation:
|
|
@@ -266,6 +269,7 @@ def create_server(
|
|
|
266
269
|
annotations=READ_ONLY_ONLINE,
|
|
267
270
|
structured_output=True,
|
|
268
271
|
)
|
|
272
|
+
@tidy
|
|
269
273
|
async def lookup_postcode(
|
|
270
274
|
postcode: Annotated[str, Field(description="Code in any style, e.g. EK-01-A03-FK-01.")],
|
|
271
275
|
ctx: Context[State, Any],
|
|
@@ -293,6 +297,7 @@ def create_server(
|
|
|
293
297
|
annotations=READ_ONLY_ONLINE,
|
|
294
298
|
structured_output=True,
|
|
295
299
|
)
|
|
300
|
+
@tidy
|
|
296
301
|
async def autocomplete_postcode(
|
|
297
302
|
partial: Annotated[
|
|
298
303
|
str, Field(min_length=1, description="The start of a code, e.g. 'EK 01 A'.")
|
|
@@ -310,9 +315,14 @@ def create_server(
|
|
|
310
315
|
annotations=READ_ONLY_ONLINE,
|
|
311
316
|
structured_output=True,
|
|
312
317
|
)
|
|
318
|
+
@tidy
|
|
313
319
|
async def find_postcode_at_location(
|
|
314
|
-
latitude: Annotated[
|
|
315
|
-
|
|
320
|
+
latitude: Annotated[
|
|
321
|
+
float, Field(ge=-90, le=90, description="Latitude in decimal degrees, e.g. 7.6211.")
|
|
322
|
+
],
|
|
323
|
+
longitude: Annotated[
|
|
324
|
+
float, Field(ge=-180, le=180, description="Longitude in decimal degrees, e.g. 5.2214.")
|
|
325
|
+
],
|
|
316
326
|
ctx: Context[State, Any],
|
|
317
327
|
max_distance_m: Annotated[
|
|
318
328
|
float | None,
|
|
@@ -333,6 +343,7 @@ def create_server(
|
|
|
333
343
|
annotations=READ_ONLY_ONLINE,
|
|
334
344
|
structured_output=True,
|
|
335
345
|
)
|
|
346
|
+
@tidy
|
|
336
347
|
async def resolve_address(
|
|
337
348
|
address: Annotated[str, Field(description="The address exactly as the user gave it.")],
|
|
338
349
|
ctx: Context[State, Any],
|
|
@@ -353,9 +364,13 @@ def create_server(
|
|
|
353
364
|
),
|
|
354
365
|
] = None,
|
|
355
366
|
latitude: Annotated[
|
|
356
|
-
float | None,
|
|
367
|
+
float | None,
|
|
368
|
+
Field(ge=-90, le=90, description="Latitude of a location pin the user shared."),
|
|
369
|
+
] = None,
|
|
370
|
+
longitude: Annotated[
|
|
371
|
+
float | None,
|
|
372
|
+
Field(ge=-180, le=180, description="Longitude of that pin. Give it with latitude."),
|
|
357
373
|
] = None,
|
|
358
|
-
longitude: Annotated[float | None, Field(ge=-180, le=180)] = None,
|
|
359
374
|
) -> Resolution:
|
|
360
375
|
"""Turn a described Nigerian address into a postcode, only as precisely as the
|
|
361
376
|
evidence allows.
|
|
@@ -380,6 +395,13 @@ def create_server(
|
|
|
380
395
|
return server
|
|
381
396
|
|
|
382
397
|
|
|
398
|
+
def tidy(tool: F) -> F:
|
|
399
|
+
"""Give the tool the same description on every Python: before 3.13 a docstring keeps
|
|
400
|
+
its indentation."""
|
|
401
|
+
tool.__doc__ = inspect.cleandoc(tool.__doc__ or "")
|
|
402
|
+
return tool
|
|
403
|
+
|
|
404
|
+
|
|
383
405
|
def reading(landmarks: list[Landmark] | None, queries: list[str] | None) -> ParsedAddress | None:
|
|
384
406
|
"""The host model's reading of the address, in the resolver's terms."""
|
|
385
407
|
if not landmarks and not queries:
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Release metadata must agree, or the registry rejects the publish."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from importlib.metadata import version
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
from mcp import Client
|
|
12
|
+
|
|
13
|
+
from ng_postcode_mcp import Settings, create_server
|
|
14
|
+
from ng_postcode_mcp.server import INSTRUCTIONS
|
|
15
|
+
|
|
16
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture
|
|
20
|
+
def anyio_backend() -> str:
|
|
21
|
+
return "asyncio"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def server_json() -> dict[str, Any]:
|
|
25
|
+
data: dict[str, Any] = json.loads((ROOT / "server.json").read_text(encoding="utf-8"))
|
|
26
|
+
return data
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_versions_and_names_agree() -> None:
|
|
30
|
+
server = server_json()
|
|
31
|
+
package = server["packages"][0]
|
|
32
|
+
assert package["identifier"] == "ng-postcode-mcp"
|
|
33
|
+
assert server["version"] == package["version"] == version("ng-postcode-mcp")
|
|
34
|
+
|
|
35
|
+
manifest = ROOT.parent / "js/packages/ng-postcode-mcp/package.json"
|
|
36
|
+
if not manifest.exists(): # the Node package lives in the repository, not the sdist
|
|
37
|
+
return
|
|
38
|
+
node = json.loads(manifest.read_text(encoding="utf-8"))
|
|
39
|
+
listed = server["packages"][1]
|
|
40
|
+
assert (listed["registryType"], listed["identifier"]) == ("npm", node["name"])
|
|
41
|
+
assert listed["version"] == node["version"]
|
|
42
|
+
assert node["mcpName"] == server["name"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_readme_proves_registry_ownership() -> None:
|
|
46
|
+
marker = f"<!-- mcp-name: {server_json()['name']} -->"
|
|
47
|
+
assert marker in (ROOT / "README.md").read_text(encoding="utf-8")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_registry_description_fits() -> None:
|
|
51
|
+
assert len(server_json()["description"]) <= 100
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_the_bundle_pins_this_version() -> None:
|
|
55
|
+
bundle = ROOT / "bundle"
|
|
56
|
+
if not bundle.exists(): # the bundle lives in the repository, not the sdist
|
|
57
|
+
return
|
|
58
|
+
released = version("ng-postcode-mcp")
|
|
59
|
+
manifest: dict[str, Any] = json.loads((bundle / "manifest.json").read_text(encoding="utf-8"))
|
|
60
|
+
assert manifest["version"] == released
|
|
61
|
+
assert f'"ng-postcode-mcp=={released}"' in (bundle / "pyproject.toml").read_text()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_the_python_bundle_pins_this_version() -> None:
|
|
65
|
+
bundle = ROOT / "bundle-python"
|
|
66
|
+
if not bundle.exists(): # the bundle lives in the repository, not the sdist
|
|
67
|
+
return
|
|
68
|
+
released = version("ng-postcode-mcp")
|
|
69
|
+
manifest: dict[str, Any] = json.loads((bundle / "manifest.json").read_text(encoding="utf-8"))
|
|
70
|
+
assert manifest["version"] == released
|
|
71
|
+
assert f'"ng-postcode-mcp=={released}"' in (bundle / "server" / "main.py").read_text()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@pytest.mark.anyio
|
|
75
|
+
async def test_the_shared_contract_matches_this_server() -> None:
|
|
76
|
+
shared = ROOT.parent / "spec" / "mcp.json"
|
|
77
|
+
if not shared.exists(): # the contract lives in the repository, not the sdist
|
|
78
|
+
return
|
|
79
|
+
async with Client(create_server(Settings(api_key=None))) as client:
|
|
80
|
+
tools = (await client.list_tools()).tools
|
|
81
|
+
live = [t.model_dump(mode="json", by_alias=True, exclude_none=True) for t in tools]
|
|
82
|
+
recorded: dict[str, Any] = json.loads(shared.read_text(encoding="utf-8"))
|
|
83
|
+
assert recorded == {"instructions": INSTRUCTIONS, "tools": live}, "run scripts/mcp_spec.py"
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
"""Release metadata must agree, or the registry rejects the publish."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
from importlib.metadata import version
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import Any
|
|
9
|
-
|
|
10
|
-
ROOT = Path(__file__).resolve().parents[1]
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def server_json() -> dict[str, Any]:
|
|
14
|
-
data: dict[str, Any] = json.loads((ROOT / "server.json").read_text(encoding="utf-8"))
|
|
15
|
-
return data
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def test_versions_and_names_agree() -> None:
|
|
19
|
-
server = server_json()
|
|
20
|
-
package = server["packages"][0]
|
|
21
|
-
assert package["identifier"] == "ng-postcode-mcp"
|
|
22
|
-
assert server["version"] == package["version"] == version("ng-postcode-mcp")
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def test_readme_proves_registry_ownership() -> None:
|
|
26
|
-
marker = f"<!-- mcp-name: {server_json()['name']} -->"
|
|
27
|
-
assert marker in (ROOT / "README.md").read_text(encoding="utf-8")
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def test_registry_description_fits() -> None:
|
|
31
|
-
assert len(server_json()["description"]) <= 100
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|