ng-postcode-mcp 0.4.1__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.1 → ng_postcode_mcp-0.4.2}/.gitignore +1 -0
- {ng_postcode_mcp-0.4.1 → ng_postcode_mcp-0.4.2}/PKG-INFO +1 -1
- {ng_postcode_mcp-0.4.1 → ng_postcode_mcp-0.4.2}/pyproject.toml +1 -1
- {ng_postcode_mcp-0.4.1 → ng_postcode_mcp-0.4.2}/server.json +31 -2
- {ng_postcode_mcp-0.4.1 → ng_postcode_mcp-0.4.2}/src/ng_postcode_mcp/server.py +15 -1
- {ng_postcode_mcp-0.4.1 → ng_postcode_mcp-0.4.2}/tests/test_metadata.py +32 -0
- {ng_postcode_mcp-0.4.1 → ng_postcode_mcp-0.4.2}/LICENSE +0 -0
- {ng_postcode_mcp-0.4.1 → ng_postcode_mcp-0.4.2}/README.md +0 -0
- {ng_postcode_mcp-0.4.1 → ng_postcode_mcp-0.4.2}/src/ng_postcode_mcp/__init__.py +0 -0
- {ng_postcode_mcp-0.4.1 → ng_postcode_mcp-0.4.2}/src/ng_postcode_mcp/__main__.py +0 -0
- {ng_postcode_mcp-0.4.1 → ng_postcode_mcp-0.4.2}/src/ng_postcode_mcp/py.typed +0 -0
- {ng_postcode_mcp-0.4.1 → ng_postcode_mcp-0.4.2}/tests/test_caller_key.py +0 -0
- {ng_postcode_mcp-0.4.1 → ng_postcode_mcp-0.4.2}/tests/test_http.py +0 -0
- {ng_postcode_mcp-0.4.1 → ng_postcode_mcp-0.4.2}/tests/test_reference_schema.py +0 -0
- {ng_postcode_mcp-0.4.1 → ng_postcode_mcp-0.4.2}/tests/test_resolve_tool.py +0 -0
- {ng_postcode_mcp-0.4.1 → ng_postcode_mcp-0.4.2}/tests/test_stdio.py +0 -0
- {ng_postcode_mcp-0.4.1 → 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
|
|
@@ -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,6 +315,7 @@ 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
320
|
latitude: Annotated[
|
|
315
321
|
float, Field(ge=-90, le=90, description="Latitude in decimal degrees, e.g. 7.6211.")
|
|
@@ -337,6 +343,7 @@ def create_server(
|
|
|
337
343
|
annotations=READ_ONLY_ONLINE,
|
|
338
344
|
structured_output=True,
|
|
339
345
|
)
|
|
346
|
+
@tidy
|
|
340
347
|
async def resolve_address(
|
|
341
348
|
address: Annotated[str, Field(description="The address exactly as the user gave it.")],
|
|
342
349
|
ctx: Context[State, Any],
|
|
@@ -388,6 +395,13 @@ def create_server(
|
|
|
388
395
|
return server
|
|
389
396
|
|
|
390
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
|
+
|
|
391
405
|
def reading(landmarks: list[Landmark] | None, queries: list[str] | None) -> ParsedAddress | None:
|
|
392
406
|
"""The host model's reading of the address, in the resolver's terms."""
|
|
393
407
|
if not landmarks and not queries:
|
|
@@ -7,9 +7,20 @@ from importlib.metadata import version
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Any
|
|
9
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
|
+
|
|
10
16
|
ROOT = Path(__file__).resolve().parents[1]
|
|
11
17
|
|
|
12
18
|
|
|
19
|
+
@pytest.fixture
|
|
20
|
+
def anyio_backend() -> str:
|
|
21
|
+
return "asyncio"
|
|
22
|
+
|
|
23
|
+
|
|
13
24
|
def server_json() -> dict[str, Any]:
|
|
14
25
|
data: dict[str, Any] = json.loads((ROOT / "server.json").read_text(encoding="utf-8"))
|
|
15
26
|
return data
|
|
@@ -21,6 +32,15 @@ def test_versions_and_names_agree() -> None:
|
|
|
21
32
|
assert package["identifier"] == "ng-postcode-mcp"
|
|
22
33
|
assert server["version"] == package["version"] == version("ng-postcode-mcp")
|
|
23
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
|
+
|
|
24
44
|
|
|
25
45
|
def test_readme_proves_registry_ownership() -> None:
|
|
26
46
|
marker = f"<!-- mcp-name: {server_json()['name']} -->"
|
|
@@ -49,3 +69,15 @@ def test_the_python_bundle_pins_this_version() -> None:
|
|
|
49
69
|
manifest: dict[str, Any] = json.loads((bundle / "manifest.json").read_text(encoding="utf-8"))
|
|
50
70
|
assert manifest["version"] == released
|
|
51
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"
|
|
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
|
|
File without changes
|