|
| 1 | +# mcp-server Helm Chart |
| 2 | + |
| 3 | +Generic runner for Model Context Protocol (MCP) servers on |
| 4 | +Kubernetes. |
| 5 | + |
| 6 | +This chart supports three modes: |
| 7 | + |
| 8 | +- `image`: run a pre-built container image that already includes your MCP |
| 9 | + server (recommended for production). |
| 10 | +- `node`: run a Node/TypeScript MCP server package via `npx` inside a |
| 11 | + `node:alpine` container. |
| 12 | +- `python`: run a Python MCP server via `uvx` (or `pip`) inside a `uv`/`python` |
| 13 | + container. |
| 14 | + |
| 15 | +## Named Servers |
| 16 | + |
| 17 | +Run many stdio‑based MCP servers behind a single HTTP(SSE) endpoint using the |
| 18 | +built‑in gateway option powered by `mcp-proxy`. |
| 19 | + |
| 20 | +What it is |
| 21 | + |
| 22 | +- One process exposes an SSE endpoint and spawns multiple stdio MCP servers as |
| 23 | + child processes. |
| 24 | +- Each server is reachable at `/servers/{name}/sse` (a default server can also |
| 25 | + be exposed at `/sse`). |
| 26 | + |
| 27 | +Why it’s useful |
| 28 | + |
| 29 | +- One Service/Ingress/Gateway for multiple stdio MCP servers. |
| 30 | +- Central place for long‑lived connection tuning (SSE/WS timeouts). |
| 31 | +- Great for internal hubs, demos, or small fleets. |
| 32 | + |
| 33 | +When not to use |
| 34 | + |
| 35 | +- If you need per‑server autoscaling/isolation, prefer multiple Deployments or |
| 36 | + an external reverse proxy. |
| 37 | + |
| 38 | +How to enable |
| 39 | + |
| 40 | +- Set `transport.type: stdio` and `transport.stdioGateway.enabled: true`. |
| 41 | +- Add entries under `transport.stdioGateway.servers` with `command`/`args`/`env`. |
| 42 | +- Or provide raw JSON via `transport.stdioGateway.namedServersJson`. |
| 43 | +- Use `transport.stdioGateway.preStart` for installing servers (e.g., pip |
| 44 | + install) or ship a custom image. |
| 45 | + |
| 46 | +Endpoints |
| 47 | + |
| 48 | +- Default: `/sse` (if you spawn a default server after `--`). |
| 49 | +- Named: `/servers/{name}/sse`. |
| 50 | +- Status: `/status`. |
| 51 | + |
| 52 | +## Prerequisites |
| 53 | + |
| 54 | +- Kubernetes 1.31+ |
| 55 | +- Helm 3.10+ |
| 56 | + |
| 57 | +## Installing the Chart |
| 58 | + |
| 59 | +To install with the release name `my-mcp` from OCI: |
| 60 | + |
| 61 | +```bash |
| 62 | +helm install my-mcp oci://ghcr.io/icoretech/charts/mcp-server |
| 63 | +``` |
| 64 | + |
| 65 | +Or from the GitHub Pages helm repo: |
| 66 | + |
| 67 | +```bash |
| 68 | +helm repo add icoretech https://icoretech.github.io/helm |
| 69 | +helm repo update |
| 70 | +helm install my-mcp icoretech/mcp-server |
| 71 | +``` |
| 72 | + |
| 73 | +## Configuration |
| 74 | + |
| 75 | +The following table lists the configurable parameters of the chart and their |
| 76 | +default values. |
| 77 | + |
| 78 | +<!-- markdownlint-disable MD013 --> |
| 79 | +## Values |
| 80 | + |
| 81 | +| Key | Type | Default | Description | |
| 82 | +|-----|------|---------|-------------| |
| 83 | +| additionalAnnotations | object | `{}` | Additional annotations applied to chart resources | |
| 84 | +| additionalLabels | object | `{}` | Additional labels applied to chart resources | |
| 85 | +| affinity | object | `{}` | Affinity rules for Pod scheduling | |
| 86 | +| autoscaling | object | `{"enabled":false,"maxReplicas":5,"minReplicas":1,"targetCPUUtilizationPercentage":80}` | Horizontal Pod Autoscaler configuration | |
| 87 | +| config.contents | string | `"# example config\n# [server]\n# port = 3000\n"` | Raw contents of the config file | |
| 88 | +| config.enabled | bool | `false` | | |
| 89 | +| config.filename | string | `"config.toml"` | Filename within the mount path (e.g., config.toml, config.yaml, config.json) | |
| 90 | +| config.mountPath | string | `"/config"` | Mount path inside the container | |
| 91 | +| container.args | list | `[]` | Override container args (array form) | |
| 92 | +| container.command | list | `[]` | Override container command (array form) | |
| 93 | +| container.env | list | `[]` | Extra environment variables | |
| 94 | +| container.extraEnvFrom | list | `[]` | Extra envFrom entries (e.g., Secret or ConfigMap refs) | |
| 95 | +| container.port | int | `3000` | Port the MCP server listens on (if using HTTP/WebSocket) | |
| 96 | +| container.workingDir | string | `""` | Working directory for the server process | |
| 97 | +| fullnameOverride | string | `""` | Completely overrides the generated name | |
| 98 | +| httpRoute.annotations | object | `{}` | | |
| 99 | +| httpRoute.enabled | bool | `false` | | |
| 100 | +| httpRoute.hostnames[0] | string | `"mcp.example.local"` | | |
| 101 | +| httpRoute.parentRefs[0].name | string | `"gateway"` | | |
| 102 | +| httpRoute.parentRefs[0].sectionName | string | `"http"` | | |
| 103 | +| httpRoute.rules | list | `[]` | | |
| 104 | +| image.args | list | `[]` | Optional override of args when mode=image (array form) | |
| 105 | +| image.command | list | `[]` | Optional override of command when mode=image (array form) | |
| 106 | +| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | |
| 107 | +| image.repository | string | `"nginx"` | Image repository | |
| 108 | +| image.tag | string | `""` | Image tag (defaults to Chart.AppVersion when empty) | |
| 109 | +| imagePullSecrets | list | `[]` | Image pull secrets for private registries | |
| 110 | +| ingress.annotations | object | `{}` | | |
| 111 | +| ingress.className | string | `""` | | |
| 112 | +| ingress.enabled | bool | `false` | | |
| 113 | +| ingress.hosts[0].host | string | `"mcp.example.local"` | | |
| 114 | +| ingress.hosts[0].paths[0].path | string | `"/"` | | |
| 115 | +| ingress.hosts[0].paths[0].pathType | string | `"ImplementationSpecific"` | | |
| 116 | +| ingress.tls | list | `[]` | | |
| 117 | +| livenessProbe | object | `{}` | Liveness probe (disabled by default; many MCP servers don’t expose HTTP health) | |
| 118 | +| mode | string | `"image"` | Runtime mode for the MCP server. One of: `image`, `node`, `python`. - `image`: run a pre-built container image (recommended for production) - `node`: run a Node/TypeScript MCP package via `npx` - `python`: run a Python MCP package via `uvx` (or pip) | |
| 119 | +| nameOverride | string | `""` | Overrides the chart name for resources | |
| 120 | +| node.args | list | `[]` | Arguments to pass to the package, e.g. ["--port", "3000"] | |
| 121 | +| node.image | string | `"node:24-alpine"` | Node base image to run npx | |
| 122 | +| node.npmrcMountPath | string | `"/home/node/.npmrc"` | Mount path for the .npmrc file | |
| 123 | +| node.npmrcSecret | string | `""` | Optional private registry auth: mount a Secret containing an ".npmrc" key | |
| 124 | +| node.package | string | `""` | npm package name, e.g. "mcp-remote" or "@acme/my-mcp-server" | |
| 125 | +| node.preStart | list | `[]` | Optional additional setup commands before starting the server | |
| 126 | +| node.pullPolicy | string | `"IfNotPresent"` | Pull policy for the Node image | |
| 127 | +| node.version | string | `"latest"` | Optional semver or dist-tag to pin, e.g. "latest" or "1.2.3" | |
| 128 | +| nodeSelector | object | `{}` | Node selector for Pod assignment | |
| 129 | +| podAnnotations | object | `{}` | Annotations added to the Pod | |
| 130 | +| podLabels | object | `{}` | Labels added to the Pod | |
| 131 | +| podSecurityContext | object | `{}` | Pod-level security context | |
| 132 | +| python.args | list | `[]` | Extra args for the package (e.g., ["--port", "3000"]) | |
| 133 | +| python.fromGit | string | `""` | Optional Git source for uvx (e.g. git+https://...). If set, `package` is executed from this source | |
| 134 | +| python.image | string | `"ghcr.io/astral-sh/uv:latest"` | Base image with uv/uvx and Python preinstalled. Alternative: python:3.12-slim | |
| 135 | +| python.package | string | `""` | uvx target, e.g. "awslabs.aws-pricing-mcp-server@latest" or a local module name | |
| 136 | +| python.preStart | list | `[]` | Optional pre-start commands (e.g., install requirements) | |
| 137 | +| python.pullPolicy | string | `"IfNotPresent"` | Pull policy for the Python image | |
| 138 | +| python.usePip | bool | `false` | Use pip instead of uvx (set to true to use pip) | |
| 139 | +| readinessProbe | object | `{}` | Readiness probe (disabled by default) | |
| 140 | +| replicaCount | int | `1` | Number of replicas for the Deployment | |
| 141 | +| resources | object | `{}` | Resource requests/limits for the container | |
| 142 | +| securityContext | object | `{}` | Container-level security context | |
| 143 | +| service.port | int | `3000` | | |
| 144 | +| service.type | string | `"ClusterIP"` | | |
| 145 | +| serviceAccount.annotations | object | `{}` | | |
| 146 | +| serviceAccount.automount | bool | `true` | | |
| 147 | +| serviceAccount.create | bool | `true` | | |
| 148 | +| serviceAccount.name | string | `""` | | |
| 149 | +| tolerations | list | `[]` | Tolerations to allow Pods to be scheduled onto nodes with taints | |
| 150 | +| transport | object | `{"http":{"path":"/sse","timeouts":{"proxySeconds":3600,"readSeconds":3600,"sendSeconds":3600},"wsPath":"/ws"},"stdioGateway":{"allowOrigins":["*"],"cwd":"","enabled":false,"env":[],"envFrom":[],"host":"0.0.0.0","image":"ghcr.io/sparfenyuk/mcp-proxy:latest","namedServersJson":"","passEnvironment":true,"port":8096,"preStart":[],"pullPolicy":"IfNotPresent","resources":{},"server":{"args":[],"command":"","cwd":"","env":[]},"servers":{}},"type":"http-sse"}` | Transport configuration | |
| 151 | +| transport.http.path | string | `"/sse"` | Base HTTP path for the MCP endpoint (e.g., `/sse` for streamable HTTP using SSE). This is used only for documentation/ingress convenience; your server must actually listen on this path. | |
| 152 | +| transport.http.timeouts | object | `{"proxySeconds":3600,"readSeconds":3600,"sendSeconds":3600}` | Recommended long-lived connection timeouts (applied as Ingress annotations where supported). | |
| 153 | +| transport.http.wsPath | string | `"/ws"` | Optional alternate WebSocket path if using `transport.type=websocket` | |
| 154 | +| transport.stdioGateway | object | `{"allowOrigins":["*"],"cwd":"","enabled":false,"env":[],"envFrom":[],"host":"0.0.0.0","image":"ghcr.io/sparfenyuk/mcp-proxy:latest","namedServersJson":"","passEnvironment":true,"port":8096,"preStart":[],"pullPolicy":"IfNotPresent","resources":{},"server":{"args":[],"command":"","cwd":"","env":[]},"servers":{}}` | Optional stdio gateway to translate stdio↔network inside the pod. Disabled by default. | |
| 155 | +| transport.stdioGateway.allowOrigins | list | `["*"]` | Add one or more CORS origins (use ["*"] for any) | |
| 156 | +| transport.stdioGateway.cwd | string | `""` | Working directory for the spawned stdio server process | |
| 157 | +| transport.stdioGateway.env | list | `[]` | Additional env just for the gateway container | |
| 158 | +| transport.stdioGateway.envFrom | list | `[]` | envFrom for the gateway container (e.g., secrets/configmaps) | |
| 159 | +| transport.stdioGateway.image | string | `"ghcr.io/sparfenyuk/mcp-proxy:latest"` | Gateway container image (defaults to a public mcp-proxy that can expose SSE and spawn a local stdio server) | |
| 160 | +| transport.stdioGateway.namedServersJson | string | `""` | Advanced: provide a raw JSON string for named servers config (overrides servers map) | |
| 161 | +| transport.stdioGateway.passEnvironment | bool | `true` | Pass all environment variables through to the spawned stdio server | |
| 162 | +| transport.stdioGateway.port | int | `8096` | Port for the gateway's SSE server to listen on (use service.port externally) | |
| 163 | +| transport.stdioGateway.preStart | list | `[]` | Optional commands to run before starting the proxy (e.g., pip installs) | |
| 164 | +| transport.stdioGateway.resources | object | `{}` | Resources for the gateway container | |
| 165 | +| transport.stdioGateway.server | object | `{"args":[],"command":"","cwd":"","env":[]}` | Optional explicit stdio server to spawn (overrides mode-based auto command) | |
| 166 | +| transport.stdioGateway.servers | object | `{}` | Define multiple named stdio servers served under `/servers/{name}/` paths. Each entry supports: command, args[], env[] (list of {name,value}), disabled (bool) | |
| 167 | +| transport.type | string | `"http-sse"` | Primary transport type exposed outside the pod. One of: `http-sse`, `websocket`, `stdio`. Note: `stdio` is generally unsuitable for remote access in Kubernetes unless you wrap the server with a gateway that translates stdio to a network transport. | |
| 168 | +| volumeMounts | list | `[]` | Additional volume mounts for the container | |
| 169 | +| volumes | list | `[]` | Additional volumes to add to the Pod | |
| 170 | +<!-- markdownlint-enable MD013 --> |
| 171 | + |
| 172 | +## Transports and Exposure |
| 173 | + |
| 174 | +Transports |
| 175 | + |
| 176 | +- `http-sse` (default): stream over a long‑lived HTTP connection. |
| 177 | +- `websocket`: stream over WebSocket. |
| 178 | +- `stdio`: intended for local clients; in Kubernetes it needs a gateway/bridge. |
| 179 | + This chart offers a stdio gateway mode using `mcp-proxy`. |
| 180 | + |
| 181 | +Exposure options |
| 182 | + |
| 183 | +- ClusterIP + port‑forward: simplest local testing. |
| 184 | +- Ingress (e.g., NGINX): add WS upgrade annotations and increase timeouts. |
| 185 | +- Gateway API (HTTPRoute): set `rules.timeouts.request/backendRequest` for |
| 186 | + long‑lived connections. |
| 187 | + |
| 188 | +Timeout tips |
| 189 | + |
| 190 | +- SSE: raise read/send/proxy timeouts (NGINX `proxy-read-timeout` and |
| 191 | + `proxy-send-timeout` to `3600`). |
| 192 | +- WebSocket: ensure upgrade support (NGINX: `enable-websocket: "true"`). |
| 193 | + |
| 194 | +## Examples |
| 195 | + |
| 196 | +Node mode (npx): |
| 197 | + |
| 198 | +```yaml |
| 199 | +mode: node |
| 200 | +node: |
| 201 | + image: node:24-alpine |
| 202 | + package: mcp-remote |
| 203 | + version: latest |
| 204 | + args: |
| 205 | + - https://docs.mcp.cloudflare.com/sse |
| 206 | + - --port |
| 207 | + - "3000" |
| 208 | +container: |
| 209 | + port: 3000 |
| 210 | +service: |
| 211 | + port: 3000 |
| 212 | +``` |
| 213 | +
|
| 214 | +Python mode (uvx): |
| 215 | +
|
| 216 | +```yaml |
| 217 | +mode: python |
| 218 | +python: |
| 219 | + image: ghcr.io/astral-sh/uv:latest |
| 220 | + package: awslabs.aws-documentation-mcp-server@latest |
| 221 | + args: |
| 222 | + - --port |
| 223 | + - "3000" |
| 224 | +container: |
| 225 | + port: 3000 |
| 226 | +service: |
| 227 | + port: 3000 |
| 228 | +``` |
| 229 | +
|
| 230 | +Stdio gateway with named servers (mcp-proxy): |
| 231 | +
|
| 232 | +```yaml |
| 233 | +# Exposes /sse for default server and /servers/{name}/sse for named servers |
| 234 | +mode: python |
| 235 | +python: |
| 236 | + image: ghcr.io/astral-sh/uv:latest |
| 237 | + package: awslabs.aws-documentation-mcp-server@latest |
| 238 | + args: |
| 239 | + - --port |
| 240 | + - "3000" |
| 241 | +service: |
| 242 | + port: 3000 |
| 243 | +transport: |
| 244 | + type: stdio |
| 245 | + stdioGateway: |
| 246 | + enabled: true |
| 247 | + image: ghcr.io/sparfenyuk/mcp-proxy:latest |
| 248 | + passEnvironment: true |
| 249 | + allowOrigins: ["*"] |
| 250 | + preStart: |
| 251 | + - pip install --no-cache-dir awslabs.aws-documentation-mcp-server awslabs.aws-pricing-mcp-server |
| 252 | + servers: |
| 253 | + docs: |
| 254 | + command: awslabs.aws-documentation-mcp-server |
| 255 | + pricing: |
| 256 | + command: awslabs.aws-pricing-mcp-server |
| 257 | +``` |
| 258 | +
|
| 259 | +WebSocket via NGINX Ingress: |
| 260 | +
|
| 261 | +```yaml |
| 262 | +service: |
| 263 | + port: 3000 |
| 264 | +container: |
| 265 | + port: 3000 |
| 266 | +transport: |
| 267 | + type: websocket |
| 268 | + http: |
| 269 | + wsPath: /ws |
| 270 | +ingress: |
| 271 | + enabled: true |
| 272 | + className: nginx |
| 273 | + annotations: |
| 274 | + nginx.ingress.kubernetes.io/enable-websocket: "true" |
| 275 | + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" |
| 276 | + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" |
| 277 | + hosts: |
| 278 | + - host: mcp.example.local |
| 279 | + paths: |
| 280 | + - path: / |
| 281 | + pathType: Prefix |
| 282 | +``` |
| 283 | +
|
| 284 | +Example values files are provided under `charts/mcp-server/examples/`: |
| 285 | + |
| 286 | +- `node-mcp-remote.yaml`: Node mode using `mcp-remote`. |
| 287 | +- `python-aws-docs.yaml`: Python mode using `awslabs.aws-documentation-mcp-server`. |
| 288 | +- `stdio-gateway-named-servers.yaml`: Stdio gateway with two named servers. |
| 289 | +- `ingress-websocket-nginx.yaml`: WebSocket transport behind NGINX Ingress. |
| 290 | +- `gateway-http.yaml`: Minimal Gateway (Gateway API) to attach HTTPRoutes. |
| 291 | +- `httproute-sse-gatewayapi.yaml`: HTTPRoute with timeouts for SSE/Streamable HTTP. |
| 292 | +- `httproute-websocket-gatewayapi.yaml`: HTTPRoute with timeouts for WebSocket. |
| 293 | + |
| 294 | +Gateway API quickstart: |
| 295 | + |
| 296 | +```bash |
| 297 | +# Install a Gateway in the same namespace as your release (default: mcp) |
| 298 | +kubectl apply -f charts/mcp-server/examples/gateway-http.yaml |
| 299 | +
|
| 300 | +# Deploy the chart with an HTTPRoute example (SSE) |
| 301 | +helm upgrade --install mcp-sse charts/mcp-server -n mcp -f charts/mcp-server/examples/httproute-sse-gatewayapi.yaml |
| 302 | +
|
| 303 | +# The route attaches to Gateway "gateway" listener "http" |
| 304 | +``` |
0 commit comments