From c85b23fd516589f63494b1cb297c793a1d9059bb Mon Sep 17 00:00:00 2001 From: Ciaran Sweet Date: Fri, 12 Jun 2026 14:14:28 +0100 Subject: [PATCH 1/2] feat!: remove dummy dataset search (testing reconcile pipeline) --- pyproject.toml | 2 - toolsets/dataset-search/pyproject.toml | 16 -- .../src/dataset_search/__init__.py | 1 - .../src/dataset_search/tools.py | 137 ------------------ .../tests/test_dataset_search.py | 44 ------ toolsets/dataset-search/toolset.yaml | 2 - uv.lock | 36 ++--- 7 files changed, 9 insertions(+), 229 deletions(-) delete mode 100644 toolsets/dataset-search/pyproject.toml delete mode 100644 toolsets/dataset-search/src/dataset_search/__init__.py delete mode 100644 toolsets/dataset-search/src/dataset_search/tools.py delete mode 100644 toolsets/dataset-search/tests/test_dataset_search.py delete mode 100644 toolsets/dataset-search/toolset.yaml diff --git a/pyproject.toml b/pyproject.toml index 80550ae..c21bd5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,6 @@ dependencies = [ "mcp-runtime", "mcp-cli", "mcp-agent", - "dataset-search", "aoi-generator", "cds", "credential-demo", @@ -32,7 +31,6 @@ members = ["packages/*", "toolsets/*"] mcp-runtime = { workspace = true } mcp-cli = { workspace = true } mcp-agent = { workspace = true } -dataset-search = { workspace = true } aoi-generator = { workspace = true } cds = { workspace = true } credential-demo = { workspace = true } diff --git a/toolsets/dataset-search/pyproject.toml b/toolsets/dataset-search/pyproject.toml deleted file mode 100644 index 983e1c9..0000000 --- a/toolsets/dataset-search/pyproject.toml +++ /dev/null @@ -1,16 +0,0 @@ -[project] -name = "dataset-search" -version = "0.1.0" -description = "Search a static catalog of C3S/CDS datasets." -requires-python = ">=3.12,<3.14" -dependencies = [ - "langchain-core<2.0.0,>=1.4.6", - "mcp-runtime", -] - -[tool.uv.sources] -mcp-runtime = { workspace = true } - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" diff --git a/toolsets/dataset-search/src/dataset_search/__init__.py b/toolsets/dataset-search/src/dataset_search/__init__.py deleted file mode 100644 index 36a43f3..0000000 --- a/toolsets/dataset-search/src/dataset_search/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Dataset search tool package.""" diff --git a/toolsets/dataset-search/src/dataset_search/tools.py b/toolsets/dataset-search/src/dataset_search/tools.py deleted file mode 100644 index 200f431..0000000 --- a/toolsets/dataset-search/src/dataset_search/tools.py +++ /dev/null @@ -1,137 +0,0 @@ -"""LangChain tools for searching a static C3S/CDS-flavoured dataset catalog.""" - -from typing import Any - -from langchain_core.tools import tool - -CATALOG: list[dict[str, Any]] = [ - { - "id": "reanalysis-era5-single-levels", - "title": "ERA5 hourly data on single levels from 1940 to present", - "description": ( - "ERA5 reanalysis providing hourly estimates of atmospheric, land " - "and oceanic climate variables on single levels, such as 2m " - "temperature, 10m wind and total precipitation." - ), - "keywords": ["reanalysis", "era5", "atmosphere", "hourly", "global"], - "temporal_coverage": "1940-01-01/present", - }, - { - "id": "reanalysis-era5-land", - "title": "ERA5-Land hourly data from 1950 to present", - "description": ( - "Enhanced land-surface reanalysis at ~9 km resolution with hourly " - "variables such as soil moisture, snow cover, surface runoff and " - "skin temperature." - ), - "keywords": ["reanalysis", "era5", "land", "soil", "hourly"], - "temporal_coverage": "1950-01-01/present", - }, - { - "id": "seasonal-original-single-levels", - "title": "Seasonal forecast daily and subdaily data on single levels", - "description": ( - "Multi-system seasonal forecasts of atmospheric variables on " - "single levels from European and international production centres." - ), - "keywords": ["seasonal", "forecast", "ensemble", "atmosphere"], - "temporal_coverage": "1981-01-01/present", - }, - { - "id": "projections-cmip6", - "title": "CMIP6 climate projections", - "description": ( - "Daily and monthly global climate projections from the Coupled " - "Model Intercomparison Project Phase 6 across historical and " - "scenario experiments." - ), - "keywords": ["projections", "cmip6", "climate", "scenarios", "models"], - "temporal_coverage": "1850-01-01/2100-12-31", - }, - { - "id": "reanalysis-cerra-single-levels", - "title": "CERRA sub-daily regional reanalysis data for Europe on single levels", - "description": ( - "Copernicus European Regional ReAnalysis at 5.5 km resolution " - "covering Europe, with surface and near-surface variables." - ), - "keywords": ["reanalysis", "cerra", "regional", "europe", "high-resolution"], - "temporal_coverage": "1984-09-01/present", - }, - { - "id": "satellite-sea-ice-concentration", - "title": "Sea ice concentration daily gridded data from satellite observations", - "description": ( - "Daily sea ice concentration for both hemispheres derived from " - "passive microwave satellite observations." - ), - "keywords": ["satellite", "sea ice", "polar", "ocean", "daily"], - "temporal_coverage": "1978-10-25/present", - }, - { - "id": "insitu-gridded-observations-europe", - "title": "E-OBS daily gridded meteorological data for Europe", - "description": ( - "Daily gridded land-only observational dataset over Europe with " - "temperature, precipitation, sea level pressure and radiation, " - "based on ECA&D station data." - ), - "keywords": ["observations", "e-obs", "europe", "gridded", "stations"], - "temporal_coverage": "1950-01-01/present", - }, - { - "id": "sis-agroclimatic-indicators", - "title": "Agroclimatic indicators derived from climate projections", - "description": ( - "Biologically relevant agroclimatic indicators (growing season " - "length, frost days, warm spells) computed from bias-adjusted " - "climate projections." - ), - "keywords": ["agriculture", "indicators", "projections", "sectoral"], - "temporal_coverage": "1951-01-01/2099-12-31", - }, -] - - -def _score(entry: dict[str, Any], terms: list[str]) -> int: - title = entry["title"].lower() - description = entry["description"].lower() - keywords = [keyword.lower() for keyword in entry["keywords"]] - score = 0 - for term in terms: - if term in title: - score += 3 - if any(term in keyword for keyword in keywords): - score += 2 - if term in description: - score += 1 - return score - - -@tool -def search_datasets(query: str, limit: int = 10) -> list[dict[str, Any]]: - """Search the catalog for datasets matching a free-text query. - - Scores keyword matches over title, keywords and description; returns the - best matches (up to `limit`) ordered by relevance. An empty result means - no dataset matched any query term. - """ - terms = query.lower().split() - scored = [(entry, _score(entry, terms)) for entry in CATALOG] - matches = sorted( - (item for item in scored if item[1] > 0), key=lambda item: -item[1] - ) - return [entry for entry, _ in matches[: max(limit, 0)]] - - -@tool -def get_dataset(dataset_id: str) -> dict[str, Any]: - """Get the full catalog entry for a dataset by its exact id.""" - for entry in CATALOG: - if entry["id"] == dataset_id: - return entry - known = ", ".join(entry["id"] for entry in CATALOG) - raise ValueError(f"unknown dataset {dataset_id!r}; known datasets: {known}") - - -TOOLS = [search_datasets, get_dataset] diff --git a/toolsets/dataset-search/tests/test_dataset_search.py b/toolsets/dataset-search/tests/test_dataset_search.py deleted file mode 100644 index a44b78d..0000000 --- a/toolsets/dataset-search/tests/test_dataset_search.py +++ /dev/null @@ -1,44 +0,0 @@ -import pytest - -from dataset_search.tools import CATALOG, get_dataset, search_datasets - - -def test_search_finds_era5(): - results = search_datasets.invoke({"query": "era5"}) - ids = [entry["id"] for entry in results] - assert "reanalysis-era5-single-levels" in ids - assert "reanalysis-era5-land" in ids - - -def test_search_ranks_title_match_first(): - results = search_datasets.invoke({"query": "sea ice"}) - assert results[0]["id"] == "satellite-sea-ice-concentration" - - -def test_search_respects_limit(): - results = search_datasets.invoke({"query": "reanalysis europe", "limit": 2}) - assert len(results) == 2 - - -def test_search_no_match_returns_empty(): - assert search_datasets.invoke({"query": "zzz-nonexistent"}) == [] - - -def test_search_empty_query_returns_empty(): - assert search_datasets.invoke({"query": ""}) == [] - - -def test_get_dataset_returns_entry(): - entry = get_dataset.invoke({"dataset_id": "projections-cmip6"}) - assert entry["title"] == "CMIP6 climate projections" - - -def test_get_dataset_unknown_id_lists_known(): - with pytest.raises(ValueError, match="known datasets"): - get_dataset.invoke({"dataset_id": "nope"}) - - -def test_catalog_entries_complete(): - for entry in CATALOG: - assert entry["id"] and entry["title"] and entry["description"] - assert entry["keywords"] and entry["temporal_coverage"] diff --git a/toolsets/dataset-search/toolset.yaml b/toolsets/dataset-search/toolset.yaml deleted file mode 100644 index a4fd46a..0000000 --- a/toolsets/dataset-search/toolset.yaml +++ /dev/null @@ -1,2 +0,0 @@ -# Optional Helm values overrides for this toolset (see charts/mcp-toolset/values.yaml). -{} diff --git a/uv.lock b/uv.lock index e9591f0..62ce0a8 100644 --- a/uv.lock +++ b/uv.lock @@ -11,7 +11,6 @@ members = [ "aoi-generator", "cds", "credential-demo", - "dataset-search", "mcp-agent", "mcp-cli", "mcp-runtime", @@ -470,21 +469,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" }, ] -[[package]] -name = "dataset-search" -version = "0.1.0" -source = { editable = "toolsets/dataset-search" } -dependencies = [ - { name = "langchain-core" }, - { name = "mcp-runtime" }, -] - -[package.metadata] -requires-dist = [ - { name = "langchain-core", specifier = ">=1.4.6,<2.0.0" }, - { name = "mcp-runtime", editable = "packages/mcp-runtime" }, -] - [[package]] name = "deprecated" version = "1.3.1" @@ -819,16 +803,16 @@ wheels = [ [[package]] name = "langchain" -version = "1.3.7" +version = "1.3.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "langgraph" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/9e/9354114081622b077e215a3ce66be6231cbe80ee022cdd7b30056749a2c5/langchain-1.3.7.tar.gz", hash = "sha256:49e1dd3a4ec96a09a2d901b5de4cf8cc69cf6bc41be76d9a838ad67a2af7982c", size = 621124, upload-time = "2026-06-10T18:20:35.036Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/71/b4980282e69b4b89cb8ad4a2ddf4c2fe71645dee014364dbf1f834a1da3b/langchain-1.3.8.tar.gz", hash = "sha256:7f34f42f9d946a8a4b0275382078dc340fe336ba6dbc1c0ebb4f225ada9853ce", size = 629985, upload-time = "2026-06-12T04:20:35.135Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/b8/12d4dce378eff231ef6a5db751380607b15d4b1feeb3397fbec043bf5f5e/langchain-1.3.7-py3-none-any.whl", hash = "sha256:7f6e13f6681068e3fb2323f9905c1f41da261c02f55337e23250de3e51d1f325", size = 131429, upload-time = "2026-06-10T18:20:33.069Z" }, + { url = "https://files.pythonhosted.org/packages/12/e9/3f6977eee9ae43483d2df76dfb999c0f7315a5ed3d90864b121b57012272/langchain-1.3.8-py3-none-any.whl", hash = "sha256:d1baf2c766ffc805aa77c59750a50ede4119794cdcee237c55f1a7df0d148953", size = 132146, upload-time = "2026-06-12T04:20:33.266Z" }, ] [[package]] @@ -954,7 +938,7 @@ wheels = [ [[package]] name = "langsmith" -version = "0.8.14" +version = "0.8.15" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -968,9 +952,9 @@ dependencies = [ { name = "xxhash" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/0c/9e4284d9c279490a2407249bd780e1075c2dd29bf6fa77e2b71e91859227/langsmith-0.8.14.tar.gz", hash = "sha256:a3e8feb178540a2866ed39faf11521a4b9e476bf94ab3acdb1491ee6f804cfda", size = 4499898, upload-time = "2026-06-10T19:55:10.607Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/2e/572ceee3ba566c86d4967fa19a146ed0a9485c2591f7e7762ecd2c03c045/langsmith-0.8.15.tar.gz", hash = "sha256:72e57c79eef82ddbbe22c5edb8bdf79eb557e65c3a1172ddfa05a724555c8294", size = 4508376, upload-time = "2026-06-12T03:02:56.118Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/06/402c8f09994d8b11600918ad19c3b210c7eb40f883c12276534ad3398225/langsmith-0.8.14-py3-none-any.whl", hash = "sha256:f4e50906449ebede47a1f01f65e035b653a3729942871d51b61396349219be7c", size = 481242, upload-time = "2026-06-10T19:55:08.728Z" }, + { url = "https://files.pythonhosted.org/packages/df/2f/30d6c24c2b1a1504a63dd554c3e3b4b75712b85cd0586395c703f8e6a633/langsmith-0.8.15-py3-none-any.whl", hash = "sha256:9cdf26495814c9ae38998cee7895cdba267b0b3566b598e407c0bb7eefb5aaf9", size = 498036, upload-time = "2026-06-12T03:02:54.186Z" }, ] [[package]] @@ -1194,7 +1178,6 @@ dependencies = [ { name = "aoi-generator" }, { name = "cds" }, { name = "credential-demo" }, - { name = "dataset-search" }, { name = "mcp-agent" }, { name = "mcp-cli" }, { name = "mcp-runtime" }, @@ -1213,7 +1196,6 @@ requires-dist = [ { name = "aoi-generator", editable = "toolsets/aoi-generator" }, { name = "cds", editable = "toolsets/cds" }, { name = "credential-demo", editable = "toolsets/credential-demo" }, - { name = "dataset-search", editable = "toolsets/dataset-search" }, { name = "mcp-agent", editable = "packages/mcp-agent" }, { name = "mcp-cli", editable = "packages/mcp-cli" }, { name = "mcp-runtime", editable = "packages/mcp-runtime" }, @@ -2600,15 +2582,15 @@ wheels = [ [[package]] name = "starlette" -version = "1.3.0" +version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c1/37/cc24e33974e1439cf5ca62b0735b63026eabb768f472d8775f52d5851ed9/starlette-1.3.0.tar.gz", hash = "sha256:bb58cbb7a699da4ee4be9ed4cdfe4bc5b0390aa6dac1d1ac714ebebe8dc3c8df", size = 2702493, upload-time = "2026-06-11T06:27:41.869Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/42/56d31c5ee52dab0ad893d67d4f9c00f5ba2b4c5d87f392eca2c3fdce01cf/starlette-1.3.0-py3-none-any.whl", hash = "sha256:ff4ca1bc23de6a45cdfbbeb9b3caaea524c9221cdd8a6684ad7a4f651a83890b", size = 73492, upload-time = "2026-06-11T06:27:40.444Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, ] [[package]] From 7bda3608ecc8929ee6a2a2504959acb833f52e85 Mon Sep 17 00:00:00 2001 From: Ciaran Sweet Date: Fri, 12 Jun 2026 14:25:01 +0100 Subject: [PATCH 2/2] fix: hard coded tests to deleted module --- README.md | 16 ++++---- packages/mcp-runtime/tests/test_server.py | 48 ++++++++++++++++------- scripts/remove-toolset | 11 ++++++ 3 files changed, 53 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 7099561..1acec78 100644 --- a/README.md +++ b/README.md @@ -34,23 +34,23 @@ uv sync # installs every workspace member into one .venv ./scripts/lint # ruff + mypy (./scripts/format to autofix) # Serve a toolset locally -TOOLSET=dataset-search uv run mcp-serve +TOOLSET=aoi-generator uv run mcp-serve # Serve more toolsets alongside it, each on its own port -TOOLSET=aoi-generator PORT=8001 uv run mcp-serve +TOOLSET=credential-demo PORT=8001 uv run mcp-serve # Talk to them from another shell uv run mcp-cli list -uv run mcp-cli call search_datasets query=era5 limit=3 +uv run mcp-cli call aoi_from_place place=alps buffer_km=25 uv run mcp-cli repl -uv run mcp-cli list --url http://localhost:8001/mcp +uv run mcp-cli call whoami --url http://localhost:8001/mcp -H "X-Demo-Token: s3cret" ``` `mcp-cli` defaults to `http://localhost:8000/mcp`; pass `--url` to point elsewhere. Each `mcp-serve` process serves exactly one toolset — the same shape as production, where every toolset is its own pod and Service. Toolsets are also importable directly (e.g. -`from dataset_search.tools import TOOLS`) for in-process use in tests, +`from aoi_generator.tools import TOOLS`) for in-process use in tests, notebooks or an agent repo. ## Adding a toolset @@ -136,11 +136,11 @@ images in GHCR (delete the package from the repo settings if you care). cluster RBAC: ```sh -kubectl -n mcp-toolsets port-forward svc/mcp-dataset-search 8000:8000 +kubectl -n mcp-toolsets port-forward svc/mcp-aoi-generator 8000:8000 uv run mcp-cli list ``` -Build an image locally with `docker build --build-arg TOOLSET=dataset-search .`. +Build an image locally with `docker build --build-arg TOOLSET=aoi-generator .`. ## Kubernetes cluster setup @@ -255,7 +255,7 @@ running: ```sh curl https:/// | jq -uv run mcp-cli list --url https:///dataset-search/mcp +uv run mcp-cli list --url https:///aoi-generator/mcp ``` The index's `connections` key is shaped for diff --git a/packages/mcp-runtime/tests/test_server.py b/packages/mcp-runtime/tests/test_server.py index 1711dc8..13207ea 100644 --- a/packages/mcp-runtime/tests/test_server.py +++ b/packages/mcp-runtime/tests/test_server.py @@ -2,6 +2,7 @@ import types import pytest +from langchain_core.tools import tool from pydantic import ValidationError from mcp_runtime.server import ( @@ -13,6 +14,22 @@ ) +@tool +def echo(text: str) -> str: + """Echo the text back.""" + return text + + +def tools_module(monkeypatch, name: str, **attrs) -> str: + """Register a synthetic tools module, so tests don't depend on which + real toolsets exist in the repo.""" + module = types.ModuleType(name) + for key, value in attrs.items(): + setattr(module, key, value) + monkeypatch.setitem(sys.modules, name, module) + return name + + def test_toolset_module_name(): assert toolset_module_name("dataset-search") == "dataset_search.tools" assert toolset_module_name("aoi-generator") == "aoi_generator.tools" @@ -56,29 +73,32 @@ def test_load_tools_missing_export(): load_tools("mcp_runtime.server") -def test_load_credential_headers(): - assert load_credential_headers("credential_demo.tools") == ["x-demo-token"] - assert load_credential_headers("dataset_search.tools") == [] +def test_load_credential_headers(monkeypatch): + declaring = tools_module( + monkeypatch, "declaring_tools", TOOLS=[echo], CREDENTIAL_HEADERS=["X-Fake"] + ) + bare = tools_module(monkeypatch, "bare_tools", TOOLS=[echo]) + assert load_credential_headers(declaring) == ["x-fake"] + assert load_credential_headers(bare) == [] def test_load_credential_headers_rejects_bad_export(monkeypatch): - module = types.ModuleType("bad_toolset_tools") - setattr(module, "CREDENTIAL_HEADERS", "x-demo-token") # noqa: B010 - not a list - monkeypatch.setitem(sys.modules, "bad_toolset_tools", module) + bad = tools_module(monkeypatch, "bad_tools", CREDENTIAL_HEADERS="x-fake") with pytest.raises(RuntimeError, match="CREDENTIAL_HEADERS"): - load_credential_headers("bad_toolset_tools") + load_credential_headers(bad) -async def test_build_server_exposes_tools(): - server = build_server("dataset-search") +async def test_build_server_derives_module_from_toolset_name(monkeypatch): + tools_module(monkeypatch, "fake_toolset.tools", TOOLS=[echo]) + server = build_server("fake-toolset") tools = await server.list_tools() - names = {tool.name for tool in tools} - assert names == {"search_datasets", "get_dataset"} + assert {tool.name for tool in tools} == {"echo"} for tool in tools: assert tool.description -async def test_build_server_module_override(): - server = build_server("anything", module_name="aoi_generator.tools") +async def test_build_server_module_override(monkeypatch): + name = tools_module(monkeypatch, "custom_tools_module", TOOLS=[echo]) + server = build_server("anything", module_name=name) tools = await server.list_tools() - assert {tool.name for tool in tools} == {"aoi_from_place", "aoi_from_point"} + assert {tool.name for tool in tools} == {"echo"} diff --git a/scripts/remove-toolset b/scripts/remove-toolset index 18d5d20..0e81621 100755 --- a/scripts/remove-toolset +++ b/scripts/remove-toolset @@ -29,3 +29,14 @@ echo "Removed $dir and deregistered it from the workspace." echo "Merge to main: the deploy workflow uninstalls the mcp-$name release." echo "Not removed automatically: out-of-band Secrets the toolset listed in" echo "toolset.yaml, and its images in GHCR." + +# Other code or docs may still reference the toolset (README examples, tests +# using it as a fixture, ...). Surface them so the removal ships clean. +pkg=$(echo "$name" | tr '-' '_') +refs=$(git grep -l -e "$name" -e "$pkg" -- ':!uv.lock' 2>/dev/null || true) +if [ -n "$refs" ]; then + echo + echo "Heads-up: '$name' is still referenced in:" + echo "$refs" | sed 's/^/ /' + echo "Update or remove those references before merging (./scripts/test)." +fi