diff --git a/README.md b/README.md index a358b4c..09b25f4 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ Table of Contents: | **[Terraform NGINX hello world](containers/terraform-nginx-hello-world/README.md)**
A minimal example running the base NGINX image in a serverless container deployed with Terraform. | N/A | [Terraform] | | **[Triggers with Terraform](containers/terraform-triggers/README.md)**
Configuring two SQS triggers, used to trigger two containers, one public, one private. | N/A | [Terraform] | | **[gRPC HTTP2 in Go](containers/grpc-http2-go/README.md)**
A Go gRPC Container using http2 | Go/Protobuf | [CLI] | +| **[Remote MCP Server](containers/mcp-go-server/README.md)**
A Go-based Model Context Protocol server exposing tools over HTTP on Serverless Containers. | Go | [Terraform]-[CLI] | | **[Rust hello world](containers/rust-hello-world)**
A simple Rust hello world Container | Rust | [CLI] | | **[.NET C#](containers/csharp-hello-world)**
A .NET C# Container hello world | C# .NET | [CLI] | | **[Ruby Hello World](containers/ruby-hello-world/)**
A simple Ruby Hello world Container | Ruby | [CLI] | diff --git a/containers/mcp-go-server/.env.example b/containers/mcp-go-server/.env.example new file mode 100644 index 0000000..e4b184c --- /dev/null +++ b/containers/mcp-go-server/.env.example @@ -0,0 +1,9 @@ +# Scaleway credentials (IAM API key) - never commit real values +SCW_ACCESS_KEY=SCWXXXXXXXXXXXXXXXXX +SCW_SECRET_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + +# Optional: Scaleway project ID (uses default project if omitted) +# SCW_DEFAULT_PROJECT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + +# Value returned by the get_app_name MCP tool +SCW_APPLICATION_NAME=my-mcp-server diff --git a/containers/mcp-go-server/.gitignore b/containers/mcp-go-server/.gitignore new file mode 100644 index 0000000..3a95676 --- /dev/null +++ b/containers/mcp-go-server/.gitignore @@ -0,0 +1,16 @@ +# Terraform +.terraform/ +terraform.tfstate* +*.tfvars +*.tfplan + +# Serverless Framework / OSLS +.serverless/ +node_modules/ + +# Go build artifacts +mcp-server +*.exe + +# Local env files +.env diff --git a/containers/mcp-go-server/Dockerfile b/containers/mcp-go-server/Dockerfile new file mode 100644 index 0000000..67a8ec0 --- /dev/null +++ b/containers/mcp-go-server/Dockerfile @@ -0,0 +1,31 @@ +# syntax=docker/dockerfile:1 + +# Step 1: Build binary using modern Go toolchain +FROM golang:1.27-alpine AS builder + +WORKDIR /app + +COPY go.mod go.sum ./ + +RUN go mod download + +COPY main.go . + +RUN CGO_ENABLED=0 GOOS=linux go build \ + -trimpath \ + -ldflags="-w -s" \ + -o mcp-server main.go + +# Step 2: Minimal non-root distroless runtime (~5MB final image) +FROM gcr.io/distroless/static-debian13:nonroot + +WORKDIR / + +COPY --from=builder /app/mcp-server /mcp-server + +EXPOSE 8080 +ENV PORT=8080 + +USER nonroot:nonroot + +ENTRYPOINT ["/mcp-server"] diff --git a/containers/mcp-go-server/README.md b/containers/mcp-go-server/README.md new file mode 100644 index 0000000..3355dee --- /dev/null +++ b/containers/mcp-go-server/README.md @@ -0,0 +1,155 @@ +# Deploying Remote MCP Servers on Scaleway Serverless Containers + +A lightweight Go implementation of the **Model Context Protocol (MCP)** deployed as a Scaleway Serverless Container, enabling AI agents (like OpenCode, Claude, and Cursor) to securely invoke tools over HTTP. + +The server is built on the official [MCP Go SDK](https://github.com/modelcontextprotocol/go-sdk) (`github.com/modelcontextprotocol/go-sdk/mcp`), which handles the JSON-RPC protocol lifecycle, tool schemas/validation, and the Streamable HTTP transport. + +The server exposes three tools: + +| Tool | Description | +| :--- | :--- | +| `get_server_time` | Returns the current UTC time from the container. | +| `echo` | Echoes back the input message. | +| `get_app_name` | Returns the `SCW_APPLICATION_NAME` environment variable. | + +--- + +## Why Serverless Fits the Model Context Protocol + +The Model Context Protocol (MCP) standardizes how LLMs interact with external tools and data sources. Hosting MCP servers on serverless platforms like Scaleway Serverless Containers offers key operational advantages: + +| Advantage | Why It Matters for MCP | +| :--- | :--- | +| **Scale-to-Zero Cost** | AI agents invoke tools in intermittent bursts. Serverless ensures you pay only for active execution time, eliminating costs when idle. | +| **Stateless Architecture** | Remote MCP uses HTTP/SSE transports where requests carry their own execution context. Serverless containers scale horizontally without sticky sessions. | +| **Sandbox Security** | Tool execution happens inside isolated, microVM-level container sandboxes, protecting infrastructure from unexpected agent actions. | +| **Zero Maintenance** | Automated OS patching, SSL termination, and horizontal scaling are handled entirely by the cloud provider. | + +### Why Scaleway Serverless Containers? + +- **Native Container Support:** Run any Docker image (Go, Rust, Python, Node.js) with zero runtime restrictions. +- **Built-in Private Access Control:** Native API Gateway enforcement of `X-Auth-Token` blocks unauthorized traffic before it hits your container — saving compute costs. +- **European Sovereignty & GDPR:** Full data processing compliance in European regions (Paris, Amsterdam, Warsaw). +- **Automated TLS & Custom Domains:** Out-of-the-box HTTPS endpoint provisioning with support for custom domain mapping. + +--- + +## Technical Architecture & Transports + +Local MCP servers communicate over standard input/output (`stdio`). Remote serverless MCP deployments require network-based transports over HTTP: + +- **Streamable HTTP / SSE:** The MCP server exposes a `/mcp` endpoint accepting POST requests for JSON-RPC 2.0 tool execution and streaming responses back over Server-Sent Events (SSE). This transport is provided by the SDK's `mcp.StreamableHTTPHandler` (deployed in `Stateless` mode, ideal for scale-to-zero containers). +- **Isolated Routing:** This example uses Go's `http.NewServeMux` (Go 1.22+ method routing) rather than the global default router, preventing route hijacking and allowing clean middleware integration. + +A `/health` endpoint is also exposed for container health checks. + +--- + +## Prerequisites + +- [Terraform](https://www.terraform.io/downloads) `>= 1.5` +- [Scaleway CLI (`scw`)](https://github.com/scaleway/scaleway-cli) (latest) +- [Docker](https://docs.docker.com/get-docker/) (for building and pushing the image) +- [Go](https://go.dev/dl/) `>= 1.22` (only needed for local development/testing) +- A Scaleway account with an [IAM API key](https://www.scaleway.com/en/docs/identity-and-access-management/iam/how-to/create-api-keys/) + +--- + +## Environment Variables + +Copy `.env.example` to `.env` and fill in your values: + +| Variable | Required | Description | +| :--- | :--- | :--- | +| `SCW_ACCESS_KEY` | Yes | Scaleway IAM access key. | +| `SCW_SECRET_KEY` | Yes | Scaleway IAM secret key. | +| `SCW_DEFAULT_PROJECT_ID` | No | Target project ID. Uses your default project if omitted. | + +```bash +cp .env.example .env +# Edit .env with your real values +``` + +--- + +## Deployment + +This example offers two deployment methods. **Choose one.** + +### Option 1: Terraform + +The `main.tf` file provisions the registry namespace, builds and pushes the Docker image, and creates/deployes the container. + +```bash +# Export your Scaleway credentials (or set them in a .env file sourced beforehand) +export SCW_ACCESS_KEY="SCWXXXXXXXXXXXXXXXXX" +export SCW_SECRET_KEY="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + +terraform init +terraform apply +``` + +When prompted, type `yes` to confirm. The apply output will print the container URL, the MCP endpoint, and the health endpoint. + +### Option 2: Scaleway CLI (`deploy.sh`) + +The `deploy.sh` script builds the Docker image, pushes it, and creates/deployes the container using only `scw` commands. + +```bash +export SCW_SECRET_KEY="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + +chmod +x deploy.sh +./deploy.sh +``` + +The script prints the container URL, MCP endpoint, and health endpoint on success. + +--- + +## Connecting Clients (e.g. OpenCode) + +Add the deployed remote endpoint to your client configuration (such as `opencode.json` or `~/.config/opencode/opencode.json`). Pass your Scaleway IAM secret key via the `X-Auth-Token` header: + +```json +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "scaleway-tools": { + "type": "remote", + "url": "https://..functions.fnc.fr-par.scw.cloud/mcp", + "headers": { + "X-Auth-Token": "YOUR_SCALEWAY_IAM_SECRET_KEY" + } + } + } +} +``` + +--- + +## Cleanup + +### Terraform + +```bash +terraform destroy +``` + +### Scaleway CLI + +The deletion commands are commented out at the bottom of `deploy.sh`. Uncomment and run them, or execute directly: + +```bash +scw container container delete region=fr-par +scw container namespace delete region=fr-par +# Optionally, remove the pushed image from the registry: +scw registry namespace delete region=fr-par +``` + +--- + +## Production Best Practices + +- **Cold Start Optimization:** Keep container images ultra-minimal (e.g., Go/Rust static binaries inside `distroless` or `alpine`) to ensure cold boots remain under 1 second. For latency-critical agents, set `min_scale = 1`. +- **Database Connection Management:** When connecting tools to databases (like PostgreSQL), use HTTP-based query clients or a connection pooler (e.g., PgBouncer) to prevent connection exhaustion during rapid container scaling. +- **Least-Privilege Tool Execution:** Restrict database user roles to read-only permissions and sanitize all inputs inside tool implementations to prevent SQL injection or accidental data manipulation. diff --git a/containers/mcp-go-server/deploy.sh b/containers/mcp-go-server/deploy.sh new file mode 100755 index 0000000..bc22872 --- /dev/null +++ b/containers/mcp-go-server/deploy.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================= +# Scaleway CLI deployment script for the mcp-go-server container example. +# Builds the Docker image, pushes it to the Scaleway Container Registry, then +# creates/deployes a Serverless Container exposing the MCP server. +# ============================================================================= + +# --- Configuration ----------------------------------------------------------- +NAMESPACE_NAME="mcp-go-server-ns" +CONTAINER_NAME="mcp-go-server" +IMAGE_NAME="mcp-server" +IMAGE_TAG="latest" +PORT=8080 +MIN_SCALE=0 +MAX_SCALE=5 +PRIVACY="private" +REGION="fr-par" +REGISTRY="rg.${REGION}.scw.cloud" + +# --- Pre-flight checks ------------------------------------------------------- +if ! command -v scw >/dev/null 2>&1; then + echo "Error: Scaleway CLI ('scw') is not installed or not in PATH." + echo "Install it from: https://github.com/scaleway/scaleway-cli" + exit 1 +fi + +if ! command -v docker >/dev/null 2>&1; then + echo "Error: 'docker' is required but was not found in PATH." + exit 1 +fi + +if ! command -v jq >/dev/null 2>&1; then + echo "Error: 'jq' is required but was not found in PATH." + exit 1 +fi + +if [[ -z "${SCW_SECRET_KEY:-}" ]]; then + echo "Error: SCW_SECRET_KEY environment variable is not set." + echo "Export your Scaleway IAM secret key before running this script." + exit 1 +fi + +# Use a per-run unique registry namespace name to avoid collisions. +# Scaleway registry namespace names must be unique per region. +REGISTRY_NAMESPACE="mcp-go-server-$(date +%s | tail -c 5)" + +# --- Docker registry login --------------------------------------------------- +echo "Logging in to Scaleway Container Registry..." +echo "${SCW_SECRET_KEY}" | docker login "${REGISTRY}" -u nologin --password-stdin + +# --- Registry namespace ------------------------------------------------------ +# The registry namespace must exist before the image can be pushed. +echo "Ensuring the registry namespace '${REGISTRY_NAMESPACE}' exists..." +REGISTRY_NAMESPACE_ID="$(scw registry namespace list \ + region="${REGION}" \ + name="${REGISTRY_NAMESPACE}" \ + -o json | jq -r '.[0].id // empty')" + +if [[ -z "${REGISTRY_NAMESPACE_ID}" ]]; then + echo "Creating registry namespace '${REGISTRY_NAMESPACE}'..." + REGISTRY_NAMESPACE_ID="$(scw registry namespace create \ + region="${REGION}" \ + name="${REGISTRY_NAMESPACE}" \ + -o json | jq -r '.id')" +fi + +# --- Build and push the image ------------------------------------------------ +FULL_IMAGE="${REGISTRY}/${REGISTRY_NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}" + +echo "Building Docker image: ${FULL_IMAGE}" +docker build -t "${FULL_IMAGE}" . + +echo "Pushing Docker image..." +docker push "${FULL_IMAGE}" + +# --- Container namespace ----------------------------------------------------- +NAMESPACE_ID="$(scw container namespace list \ + region="${REGION}" \ + name="${NAMESPACE_NAME}" \ + -o json | jq -r '.[0].id // empty')" + +if [[ -z "${NAMESPACE_ID}" ]]; then + echo "Creating container namespace '${NAMESPACE_NAME}'..." + NAMESPACE_ID="$(scw container namespace create \ + region="${REGION}" \ + name="${NAMESPACE_NAME}" \ + --wait \ + -o json | jq -r '.id')" +else + echo "Reusing existing namespace '${NAMESPACE_NAME}' (${NAMESPACE_ID})." +fi + +# --- Container --------------------------------------------------------------- +echo "Creating container '${CONTAINER_NAME}'..." +CONTAINER_ID="$(scw container container create \ + region="${REGION}" \ + namespace-id="${NAMESPACE_ID}" \ + name="${CONTAINER_NAME}" \ + image="${FULL_IMAGE}" \ + port="${PORT}" \ + min-scale="${MIN_SCALE}" \ + max-scale="${MAX_SCALE}" \ + privacy="${PRIVACY}" \ + --wait \ + -o json | jq -r '.id')" + +# --- Output ------------------------------------------------------------------ +DOMAIN="$(scw container container get \ + region="${REGION}" \ + "${CONTAINER_ID}" \ + -o json | jq -r '.public_endpoint')" + +echo "" +echo "Container URL: ${DOMAIN}" +echo "MCP endpoint: ${DOMAIN}/mcp" +echo "Health endpoint: ${DOMAIN}/health" + +# ============================================================================= +# Cleanup / Remove +# ----------------------------------------------------------------------------- +# To delete the container and namespace created above, uncomment and run: +# +# scw container container delete region="${REGION}" "${CONTAINER_ID}" +# scw container namespace delete region="${REGION}" "${NAMESPACE_ID}" +# +# To also remove the pushed Docker image from the registry: +# +# scw registry namespace delete region="${REGION}" "${REGISTRY_NAMESPACE_ID}" +# ============================================================================= diff --git a/containers/mcp-go-server/go.mod b/containers/mcp-go-server/go.mod new file mode 100644 index 0000000..178f3ab --- /dev/null +++ b/containers/mcp-go-server/go.mod @@ -0,0 +1,16 @@ +module github.com/scaleway/serverless-examples/containers/mcp-go-server + +go 1.27.0 + +require github.com/modelcontextprotocol/go-sdk v1.7.0 + +require ( + github.com/google/jsonschema-go v0.4.3 // indirect + github.com/segmentio/asm v1.1.3 // indirect + github.com/segmentio/encoding v0.5.4 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect + golang.org/x/oauth2 v0.35.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/time v0.15.0 // indirect +) diff --git a/containers/mcp-go-server/go.sum b/containers/mcp-go-server/go.sum new file mode 100644 index 0000000..f93aba1 --- /dev/null +++ b/containers/mcp-go-server/go.sum @@ -0,0 +1,24 @@ +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0= +github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/modelcontextprotocol/go-sdk v1.7.0 h1:yqjY2dsbKAC0LSuWZVBMrHgiG8ukXv6NRo0JiALay44= +github.com/modelcontextprotocol/go-sdk v1.7.0/go.mod h1:dL7u98E/zjJTGzEq+j30jQ8K2k1mb6LeAH4inEcSGts= +github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= +github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= +github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= +github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= diff --git a/containers/mcp-go-server/main.go b/containers/mcp-go-server/main.go new file mode 100644 index 0000000..fef933c --- /dev/null +++ b/containers/mcp-go-server/main.go @@ -0,0 +1,94 @@ +package main + +import ( + "context" + "log/slog" + "net/http" + "os" + "time" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +type echoArgs struct { + Message string `json:"message" jsonschema:"The string to echo back"` +} + +func main() { + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + server := mcp.NewServer(&mcp.Implementation{ + Name: "scaleway-test-mcp", + Version: "1.1.0", + }, nil) + + mcp.AddTool(server, &mcp.Tool{ + Name: "get_server_time", + Description: "Returns current UTC time from the Scaleway container", + }, func(_ context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) { + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{ + Text: "Scaleway Container UTC Time: " + time.Now().UTC().Format(time.RFC3339), + }}, + }, nil, nil + }) + + mcp.AddTool(server, &mcp.Tool{ + Name: "echo", + Description: "Echoes back the input message", + }, func(_ context.Context, req *mcp.CallToolRequest, in echoArgs) (*mcp.CallToolResult, any, error) { + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{ + Text: "Echo from Scaleway: " + in.Message, + }}, + }, nil, nil + }) + + mcp.AddTool(server, &mcp.Tool{ + Name: "get_app_name", + Description: "Returns the SCW_APPLICATION_NAME environment variable from the container", + }, func(_ context.Context, _ *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) { + appName := os.Getenv("SCW_APPLICATION_NAME") + if appName == "" { + appName = "[NOT_SET]" + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{ + Text: "SCW_APPLICATION_NAME: " + appName, + }}, + }, nil, nil + }) + + mux := http.NewServeMux() + + mux.HandleFunc("GET /health", handleHealth) + mux.Handle("/mcp", mcp.NewStreamableHTTPHandler(func(_ *http.Request) *mcp.Server { + return server + }, &mcp.StreamableHTTPOptions{ + Stateless: true, + DisableLocalhostProtection: true, // auth is enforced upstream by the Scaleway platform + })) + + slog.Info("Starting MCP server", "port", port) + + httpServer := &http.Server{ + Addr: ":" + port, + ReadHeaderTimeout: 10 * time.Second, + Handler: mux, + } + + err := httpServer.ListenAndServe() + if err != nil { + slog.Error("Server crashed", "error", err) + os.Exit(1) + } +} + +func handleHealth(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("OK")) +} diff --git a/containers/mcp-go-server/main.tf b/containers/mcp-go-server/main.tf new file mode 100644 index 0000000..ae51686 --- /dev/null +++ b/containers/mcp-go-server/main.tf @@ -0,0 +1,114 @@ +############################################################################### +# Terraform configuration for the mcp-go-server Serverless Container example. +# +# Deploys a lightweight Go-based MCP (Model Context Protocol) server on Scaleway +# Serverless Containers. The Docker image is built locally and pushed to the +# Scaleway Container Registry, then referenced by the container. +############################################################################### + +terraform { + required_version = ">= 1.5" + + required_providers { + scaleway = { + source = "scaleway/scaleway" + version = "~> 2.0" + } + random = { + source = "hashicorp/random" + version = "~> 3.0" + } + } +} + +# Credentials (SCW_ACCESS_KEY, SCW_SECRET_KEY) and the target project +# (SCW_DEFAULT_PROJECT_ID) are read from the environment by the provider. +provider "scaleway" { + region = "fr-par" +} + +locals { + name_prefix = "mcp-go-server" + tags = ["serverless-examples", "mcp", "go", "terraform"] + image_name = "mcp-server" + image_tag = "latest" + container_port = 8080 + min_scale = 0 + max_scale = 5 +} + +# Scaleway registry namespace names are unique per-region, +# so we add a short random suffix to avoid name collisions. +resource "random_string" "registry_suffix" { + length = 4 + lower = true + upper = false + numeric = true + special = false +} + +resource "scaleway_registry_namespace" "main" { + name = "${local.name_prefix}-registry-${random_string.registry_suffix.result}" +} + +resource "scaleway_container_namespace" "main" { + name = "${local.name_prefix}-ns" + description = "Namespace for the MCP Go server example" + tags = local.tags +} + +# Build and push the Docker image to the Scaleway Container Registry. +# Note: in a production CI/CD pipeline, you would typically build and push the +# image outside of Terraform. This null_resource keeps the example self-contained. +resource "null_resource" "build_and_push" { + depends_on = [scaleway_registry_namespace.main] + + triggers = { + image_tag = timestamp() # Force rebuild on every apply + } + + provisioner "local-exec" { + command = <<-EOT + set -euo pipefail + # $${...} is escaped so the shell expands SCW_SECRET_KEY from the + # environment at apply time (Terraform otherwise treats it as a reference). + docker login "${scaleway_registry_namespace.main.endpoint}" -u nologin -p "$${SCW_SECRET_KEY}" + docker build --platform linux/amd64 -t "${scaleway_registry_namespace.main.endpoint}/${local.image_name}:${local.image_tag}" . + docker push "${scaleway_registry_namespace.main.endpoint}/${local.image_name}:${local.image_tag}" + EOT + } +} + +resource "scaleway_container" "main" { + depends_on = [null_resource.build_and_push] + + name = local.name_prefix + namespace_id = scaleway_container_namespace.main.id + registry_image = "${scaleway_registry_namespace.main.endpoint}/${local.image_name}:${local.image_tag}" + port = local.container_port + tags = local.tags + + cpu_limit = 1000 + memory_limit_bytes = 1024 * 1024 * 1024 # 1 GiB + min_scale = local.min_scale + max_scale = local.max_scale + + # WARN: Set privacy = "private" to enforces IAM token authentication via the API Gateway, + # so only callers with a valid X-Auth-Token header reach the container. + privacy = "public" +} + +output "container_domain" { + description = "Public HTTPS endpoint of the deployed MCP server." + value = scaleway_container.main.public_endpoint +} + +output "mcp_endpoint" { + description = "Full URL of the /mcp JSON-RPC endpoint." + value = "${scaleway_container.main.public_endpoint}/mcp" +} + +output "health_endpoint" { + description = "Health check endpoint." + value = "${scaleway_container.main.public_endpoint}/health" +}