Skip to content

Commit 0a70285

Browse files
masterkainqwencoder
andcommitted
feat(mcp-server): add configurable MCP server chart (SSE/WS/stdio, optional mcp-proxy)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
1 parent 17ee37d commit 0a70285

26 files changed

+1658
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ For detailed information on the individual charts and their usage, please naviga
1111
- [Airbroke](https://icoretech.github.io/helm/charts/airbroke): A modern, React-based open-source error catcher web application.
1212
- [PgBouncer](https://icoretech.github.io/helm/charts/pgbouncer): A lightweight connection pooler for PostgreSQL.
1313
- [Next.js](https://icoretech.github.io/helm/charts/nextjs): Generic, no-database Helm chart for Next.js standalone apps.
14+
- [MCP Server](https://icoretech.github.io/helm/charts/mcp-server): Generic runner for MCP servers (image, Node via npx, Python via uvx/pip).
1415

1516
## Deprecated Charts
1617

charts/mcp-server/.helmignore

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Patterns to ignore when building packages.
2+
# This supports shell glob matching, relative path matching, and
3+
# negation (prefixed with !). Only one pattern per line.
4+
.DS_Store
5+
# Common VCS dirs
6+
.git/
7+
.gitignore
8+
.bzr/
9+
.bzrignore
10+
.hg/
11+
.hgignore
12+
.svn/
13+
# Common backup files
14+
*.swp
15+
*.bak
16+
*.tmp
17+
*.orig
18+
*~
19+
# Various IDEs
20+
.project
21+
.idea/
22+
*.tmproj
23+
.vscode/

charts/mcp-server/Chart.yaml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
apiVersion: v2
2+
name: mcp-server
3+
description: "Generic MCP server runner for Node (TypeScript) and Python servers. Supports three modes: direct container image, Node (npx), and Python (uvx/pip)."
4+
5+
# A chart can be either an 'application' or a 'library' chart.
6+
#
7+
# Application charts are a collection of templates that can be packaged into versioned archives
8+
# to be deployed.
9+
#
10+
# Library charts provide useful utilities or functions for the chart developer. They're included as
11+
# a dependency of application charts to inject those utilities and functions into the rendering
12+
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
13+
type: application
14+
15+
# This is the chart version. This version number should be incremented each time you make changes
16+
# to the chart and its templates, including the app version.
17+
# Versions are expected to follow Semantic Versioning (https://semver.org/)
18+
version: 0.1.0
19+
20+
# This is the version number of the application being deployed. This version number should be
21+
# incremented each time you make changes to the application. Versions are not expected to
22+
# follow Semantic Versioning. They should reflect the version the application is using.
23+
# It is recommended to use it with quotes.
24+
appVersion: "0.0.1"
25+
icon: https://icoretech.github.io/helm/charts/mcp-server/logo.svg
26+
keywords:
27+
- mcp
28+
- model-context-protocol
29+
- server
30+
maintainers:
31+
- name: Claudio Poli
32+
email: claudio@icorete.ch
33+
url: https://github.com/masterkain
34+
home: https://github.com/icoretech/helm
35+
sources:
36+
- https://github.com/icoretech/helm/tree/main/charts/mcp-server
37+
annotations:
38+
artifacthub.io/license: "MIT"
39+
artifacthub.io/links: |
40+
- name: Source
41+
url: https://github.com/icoretech/helm/tree/main/charts/mcp-server
42+
- name: MCP Spec
43+
url: https://modelcontextprotocol.io/
44+
artifacthub.io/changes: |
45+
- kind: added
46+
description: Initial release with image, node (npx) and python (uvx/pip) modes
47+
artifacthub.io/prerelease: "false"

charts/mcp-server/README.md

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
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

Comments
 (0)