The index can only discover toolsets through the Kubernetes API. Every path into mcp_index — the token it authenticates with, the host it queries, the shape it parses — assumes a pod in a cluster, so an identical deployment anywhere else serves an empty directory or nothing at all.
Where this came from
mcp-toolsets has requests to deploy on AWS without EKS: ECS on Fargate, with the infrastructure managed by CDK. The template would offer the choice at bootstrap, keep whichever target the user picks, and behave the same either way. Everything else in that port is a translation — a Deployment becomes a Fargate service, an Ingress rule becomes a listener rule, cert-manager becomes ACM. The index is the one piece with no translation available, because the discovery is not configuration, it is the module.
SERVICE_ACCOUNT_DIR = Path("/var/run/secrets/kubernetes.io/serviceaccount")
TOOLSET_LABEL = "mcp-toolsets/toolset"
async def fetch_toolset_services(client: httpx.AsyncClient) -> list[ToolsetService]:
"""List toolset Services in this pod's namespace via the Kubernetes API."""
namespace = (SERVICE_ACCOUNT_DIR / "namespace").read_text().strip()
token = (SERVICE_ACCOUNT_DIR / "token").read_text().strip()
response = await client.get(
f"https://kubernetes.default.svc/api/v1/namespaces/{namespace}/services",
...
)
Off-cluster, the token file is absent and the read raises before any request is made. There is no fallback and no way to supply the answer from outside.
What discovery is actually for
Worth naming, because it decides the fix. The index does not just need a list of URLs — it needs a list of what is running, which is why it queries the platform rather than reading a manifest. A static list injected at deploy time would drift the moment a service failed to start, and the directory would report a toolset that isn't there. Whatever replaces this has to keep asking the platform.
Proposed
Split the lookup behind a setting, with today's behaviour as the default:
MCP_INDEX_DISCOVERY=kubernetes # default, unchanged
MCP_INDEX_DISCOVERY=ecs
MCP_ECS_CLUSTER=<cluster> # ecs only
The ECS backend lists services on the named cluster, keeps the ones tagged mcp-toolsets/toolset (the same selector, a different mechanism), and builds each internal base URL from the service's Cloud Map registration. It needs ecs:ListServices, ecs:DescribeServices, servicediscovery:ListServices and servicediscovery:DiscoverInstances on the task role.
boto3 goes behind an optional extra — mcp-toolsets-runtime[aws] — imported only when that backend is selected, alongside the extras already published for agent, web, state and checkpointing-postgres. A cluster deployment installs nothing new. Selecting ecs without the extra should fail at startup with a message naming it, not at the first request.
toolset_services() is already a pure function over a payload, so the parsing half is testable per backend without either platform.
Not in scope
The public URL shape ({public_url}/{toolset}/mcp) stays exactly as it is. Making a load balancer able to serve that shape is a separate change — see the path prefix issue.
The index can only discover toolsets through the Kubernetes API. Every path into
mcp_index— the token it authenticates with, the host it queries, the shape it parses — assumes a pod in a cluster, so an identical deployment anywhere else serves an empty directory or nothing at all.Where this came from
mcp-toolsetshas requests to deploy on AWS without EKS: ECS on Fargate, with the infrastructure managed by CDK. The template would offer the choice at bootstrap, keep whichever target the user picks, and behave the same either way. Everything else in that port is a translation — a Deployment becomes a Fargate service, an Ingress rule becomes a listener rule, cert-manager becomes ACM. The index is the one piece with no translation available, because the discovery is not configuration, it is the module.Off-cluster, the token file is absent and the read raises before any request is made. There is no fallback and no way to supply the answer from outside.
What discovery is actually for
Worth naming, because it decides the fix. The index does not just need a list of URLs — it needs a list of what is running, which is why it queries the platform rather than reading a manifest. A static list injected at deploy time would drift the moment a service failed to start, and the directory would report a toolset that isn't there. Whatever replaces this has to keep asking the platform.
Proposed
Split the lookup behind a setting, with today's behaviour as the default:
The ECS backend lists services on the named cluster, keeps the ones tagged
mcp-toolsets/toolset(the same selector, a different mechanism), and builds each internal base URL from the service's Cloud Map registration. It needsecs:ListServices,ecs:DescribeServices,servicediscovery:ListServicesandservicediscovery:DiscoverInstanceson the task role.boto3goes behind an optional extra —mcp-toolsets-runtime[aws]— imported only when that backend is selected, alongside the extras already published foragent,web,stateandcheckpointing-postgres. A cluster deployment installs nothing new. Selectingecswithout the extra should fail at startup with a message naming it, not at the first request.toolset_services()is already a pure function over a payload, so the parsing half is testable per backend without either platform.Not in scope
The public URL shape (
{public_url}/{toolset}/mcp) stays exactly as it is. Making a load balancer able to serve that shape is a separate change — see the path prefix issue.