vars:
  clusterName:
    default: "fastmcp-standalone"
    description: "Name of the Kind cluster"
  imageRepository:
    default: "python"
    description: "Python Docker image repository used to run the server"
  imageTag:
    default: "3.14-slim"
    description: "Python Docker image tag"
  fastmcpVersion:
    default: "2.14.7"
    description: "FastMCP version the server is built on, installed at startup"
  authToken:
    default: ""
    description: "Static bearer token callers must present; empty disables authentication"
  tools:
    default: ""
    description: "Comma-separated roster of tools to expose; empty exposes every tool"
images:
  preload:
    refs:
      - "{{ .imageRepository }}:{{ .imageTag }}"
kind:
  name: "{{ .clusterName }}"
  nodes:
    - role: control-plane
      extraPortMappings:
        - containerPort: 30800
          hostPort: 30800
components:
  - name: fastmcp
    type: k8s
    k8s:
      manifests:
        - apiVersion: v1
          kind: ConfigMap
          metadata:
            name: fastmcp
            labels:
              app: fastmcp
          data:
            server.py: |
              # MCP test server for e2e-testing MCP proxies and gateways.
              # Serves Streamable HTTP on 0.0.0.0:8000/mcp, plus GET /health
              # and an /admin API to enable or disable tools at runtime.
              import os

              from starlette.requests import Request
              from starlette.responses import JSONResponse

              from fastmcp import FastMCP

              SERVER_NAME = os.environ.get("MCP_SERVER_NAME", "fastmcp")
              AUTH_TOKEN = os.environ.get("MCP_AUTH_TOKEN", "")
              PORT = int(os.environ.get("PORT", "8000"))

              auth = None
              if AUTH_TOKEN:
                  from fastmcp.server.auth.providers.jwt import StaticTokenVerifier

                  auth = StaticTokenVerifier(
                      tokens={AUTH_TOKEN: {"client_id": "gravitee-e2e", "scopes": []}}
                  )

              mcp = FastMCP(SERVER_NAME, auth=auth)


              # Every tool is deterministic: same input, same output.

              def echo(message: str) -> str:
                  """Echo the message back unchanged."""
                  return message


              def add(a: float, b: float) -> float:
                  """Add two numbers."""
                  return a + b


              def multiply(a: float, b: float) -> float:
                  """Multiply two numbers."""
                  return a * b


              def to_upper(text: str) -> str:
                  """Uppercase the text."""
                  return text.upper()


              def concat(left: str, right: str) -> str:
                  """Concatenate two strings."""
                  return left + right


              def word_count(text: str) -> int:
                  """Count the words in the text."""
                  return len(text.split())


              def reverse(text: str) -> str:
                  """Reverse the text."""
                  return text[::-1]


              ALL_TOOLS = dict(
                  echo=echo,
                  add=add,
                  multiply=multiply,
                  to_upper=to_upper,
                  concat=concat,
                  word_count=word_count,
                  reverse=reverse,
              )

              roster = os.environ.get("MCP_TOOLS") or ",".join(ALL_TOOLS)
              registered = dict()
              for name in [n.strip() for n in roster.split(",") if n.strip()]:
                  registered[name] = mcp.tool(ALL_TOOLS[name])


              def hidden_tool() -> str:
                  """A tool that only exists once enabled through the admin API."""
                  return "now you see me"


              # Dormant until POST /admin/tools/hidden_tool/enable: lets a test
              # observe an upstream tool appearing after registration.
              registered["hidden_tool"] = mcp.tool(hidden_tool, enabled=False)


              @mcp.resource("data://config")
              def config() -> dict:
                  """Static server configuration."""
                  return {"server": SERVER_NAME, "authenticated": bool(AUTH_TOKEN)}


              @mcp.resource("users://{user_id}/profile")
              def user_profile(user_id: str) -> dict:
                  """Profile of the given user."""
                  return {"id": user_id, "name": f"User {user_id}", "status": "active"}


              @mcp.prompt
              def summarize(text: str) -> str:
                  """Build a prompt asking for a short summary."""
                  return f"Summarize the following text in one sentence:\n\n{text}"


              @mcp.prompt
              def analyze_data(topic: str) -> str:
                  """Build a prompt asking for an analysis of a topic."""
                  return f"Analyze the data about {topic} and list three key findings."


              @mcp.custom_route("/health", methods=["GET"])
              async def health(request: Request) -> JSONResponse:
                  return JSONResponse({"status": "healthy", "server": SERVER_NAME})


              @mcp.custom_route("/admin/tools/{name}/enable", methods=["POST"])
              async def enable_tool(request: Request) -> JSONResponse:
                  return _toggle(request.path_params["name"], enabled=True)


              @mcp.custom_route("/admin/tools/{name}/disable", methods=["POST"])
              async def disable_tool(request: Request) -> JSONResponse:
                  return _toggle(request.path_params["name"], enabled=False)


              def _toggle(name: str, enabled: bool) -> JSONResponse:
                  tool = registered.get(name)
                  if tool is None:
                      return JSONResponse({"error": f"unknown tool: {name}"}, status_code=404)
                  if enabled:
                      tool.enable()
                  else:
                      tool.disable()
                  return JSONResponse({"tool": name, "enabled": enabled})


              if __name__ == "__main__":
                  mcp.run(transport="http", host="0.0.0.0", port=PORT)
        - apiVersion: apps/v1
          kind: Deployment
          metadata:
            name: fastmcp
            labels:
              app: fastmcp
          spec:
            replicas: 1
            selector:
              matchLabels:
                app: fastmcp
            template:
              metadata:
                labels:
                  app: fastmcp
              spec:
                # The service is named fastmcp, so service links would inject
                # FASTMCP_PORT=tcp://... into the pod — which FastMCP's own
                # FASTMCP_-prefixed settings reader tries to parse as its
                # port and crashes on.
                enableServiceLinks: false
                containers:
                  - name: fastmcp
                    image: "{{ .imageRepository }}:{{ .imageTag }}"
                    command: ["sh", "-c"]
                    args:
                      - pip install --no-cache-dir --quiet fastmcp=={{ .fastmcpVersion }} && exec python /app/server.py
                    env:
                      - name: MCP_SERVER_NAME
                        value: fastmcp
                      - name: MCP_TOOLS
                        value: "{{ .tools }}"
                      - name: MCP_AUTH_TOKEN
                        value: "{{ .authToken }}"
                    ports:
                      - containerPort: 8000
                        name: http
                    startupProbe:
                      httpGet:
                        path: /health
                        port: 8000
                      periodSeconds: 2
                      failureThreshold: 90
                    readinessProbe:
                      httpGet:
                        path: /health
                        port: 8000
                    livenessProbe:
                      httpGet:
                        path: /health
                        port: 8000
                    volumeMounts:
                      - name: server
                        mountPath: /app
                    resources:
                      requests:
                        cpu: 100m
                        memory: 128Mi
                      limits:
                        cpu: 500m
                        memory: 512Mi
                volumes:
                  - name: server
                    configMap:
                      name: fastmcp
        - apiVersion: v1
          kind: Service
          metadata:
            name: fastmcp
            labels:
              app: fastmcp
          spec:
            type: NodePort
            ports:
              - port: 8000
                targetPort: 8000
                nodePort: 30800
                name: http
            selector:
              app: fastmcp
