Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -255,7 +255,7 @@ running:

```sh
curl https://<host>/ | jq
uv run mcp-cli list --url https://<host>/dataset-search/mcp
uv run mcp-cli list --url https://<host>/aoi-generator/mcp
```

The index's `connections` key is shaped for
Expand Down
48 changes: 34 additions & 14 deletions packages/mcp-runtime/tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import types

import pytest
from langchain_core.tools import tool
from pydantic import ValidationError

from mcp_runtime.server import (
Expand All @@ -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"
Expand Down Expand Up @@ -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"}
2 changes: 0 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ dependencies = [
"mcp-runtime",
"mcp-cli",
"mcp-agent",
"dataset-search",
"aoi-generator",
"cds",
"credential-demo",
Expand All @@ -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 }
Expand Down
11 changes: 11 additions & 0 deletions scripts/remove-toolset
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if everyone will remember running this when removing a tool. But its ok to have it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the block below searches for references that weren't deleted and tells the invoker that 🫡

# 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
16 changes: 0 additions & 16 deletions toolsets/dataset-search/pyproject.toml

This file was deleted.

1 change: 0 additions & 1 deletion toolsets/dataset-search/src/dataset_search/__init__.py

This file was deleted.

137 changes: 0 additions & 137 deletions toolsets/dataset-search/src/dataset_search/tools.py

This file was deleted.

44 changes: 0 additions & 44 deletions toolsets/dataset-search/tests/test_dataset_search.py

This file was deleted.

2 changes: 0 additions & 2 deletions toolsets/dataset-search/toolset.yaml

This file was deleted.

Loading
Loading