2 Commits

Author SHA1 Message Date
5da307cab5 Add Dockerfile and docker-master build target
Two-stage build: golang:1.25-alpine builder, alpine:3.21 runtime.
Produces a minimal container image for mcp-master.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:52:10 -07:00
22a836812f Add public, tier, node fields to ServiceDef
RouteDef gains Public field (bool) for edge routing. ServiceDef gains
Tier field. Node validation relaxed: defaults to tier=worker when both
node and tier are empty (v2 compatibility).

ToProto/FromProto updated to round-trip all new fields. Without this,
public=true in TOML was silently dropped and edge routing never triggered.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:42:00 -07:00
4 changed files with 48 additions and 16 deletions

22
Dockerfile.master Normal file
View File

@@ -0,0 +1,22 @@
FROM golang:1.25-alpine AS builder
ARG VERSION=dev
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.version=${VERSION}" \
-o /mcp-master ./cmd/mcp-master
FROM alpine:3.21
RUN apk add --no-cache ca-certificates tzdata
COPY --from=builder /mcp-master /usr/local/bin/mcp-master
WORKDIR /srv/mcp-master
EXPOSE 9555
ENTRYPOINT ["mcp-master"]
CMD ["server", "--config", "/srv/mcp-master/mcp-master.toml"]

View File

@@ -32,6 +32,11 @@ proto-lint:
buf lint
buf breaking --against '.git#branch=master,subdir=proto'
docker-master:
podman build -f Dockerfile.master \
--build-arg VERSION=$(shell git describe --tags --always --dirty) \
-t mcr.svc.mcp.metacircular.net:8443/mcp-master:$(shell git describe --tags --always --dirty) .
clean:
rm -f mcp mcp-agent mcp-master

View File

@@ -16,8 +16,10 @@ import (
// ServiceDef is the top-level TOML structure for a service definition file.
type ServiceDef struct {
Name string `toml:"name"`
Node string `toml:"node"`
Node string `toml:"node,omitempty"`
Tier string `toml:"tier,omitempty"`
Active *bool `toml:"active,omitempty"`
Comment string `toml:"comment,omitempty"`
Path string `toml:"path,omitempty"`
Build *BuildDef `toml:"build,omitempty"`
Components []ComponentDef `toml:"components"`
@@ -36,6 +38,7 @@ type RouteDef struct {
Port int `toml:"port"`
Mode string `toml:"mode,omitempty"`
Hostname string `toml:"hostname,omitempty"`
Public bool `toml:"public,omitempty"`
}
// ComponentDef describes a single container component within a service.
@@ -129,8 +132,9 @@ func validate(def *ServiceDef) error {
if def.Name == "" {
return fmt.Errorf("service name is required")
}
if def.Node == "" {
return fmt.Errorf("service node is required")
// v2: either node or tier must be set. Tier defaults to "worker" if both empty.
if def.Node == "" && def.Tier == "" {
def.Tier = "worker"
}
if len(def.Components) == 0 {
return fmt.Errorf("service %q must have at least one component", def.Name)
@@ -191,8 +195,11 @@ func validateRoutes(compName, svcName string, routes []RouteDef) error {
// ToProto converts a ServiceDef to a proto ServiceSpec.
func ToProto(def *ServiceDef) *mcpv1.ServiceSpec {
spec := &mcpv1.ServiceSpec{
Name: def.Name,
Active: def.Active != nil && *def.Active,
Name: def.Name,
Active: def.Active != nil && *def.Active,
Comment: def.Comment,
Tier: def.Tier,
Node: def.Node,
}
for _, c := range def.Components {
@@ -213,6 +220,7 @@ func ToProto(def *ServiceDef) *mcpv1.ServiceSpec {
Port: int32(r.Port), //nolint:gosec // port range validated
Mode: r.Mode,
Hostname: r.Hostname,
Public: r.Public,
})
}
spec.Components = append(spec.Components, cs)
@@ -227,9 +235,11 @@ func ToProto(def *ServiceDef) *mcpv1.ServiceSpec {
func FromProto(spec *mcpv1.ServiceSpec, node string) *ServiceDef {
active := spec.GetActive()
def := &ServiceDef{
Name: spec.GetName(),
Node: node,
Active: &active,
Name: spec.GetName(),
Node: node,
Tier: spec.GetTier(),
Active: &active,
Comment: spec.GetComment(),
}
for _, c := range spec.GetComponents() {
@@ -250,6 +260,7 @@ func FromProto(spec *mcpv1.ServiceSpec, node string) *ServiceDef {
Port: int(r.GetPort()),
Mode: r.GetMode(),
Hostname: r.GetHostname(),
Public: r.GetPublic(),
})
}
def.Components = append(def.Components, cd)

View File

@@ -119,14 +119,8 @@ func TestValidation(t *testing.T) {
},
wantErr: "service name is required",
},
{
name: "missing node",
def: &ServiceDef{
Name: "svc",
Components: []ComponentDef{{Name: "api", Image: "img:v1"}},
},
wantErr: "service node is required",
},
// v2: missing node no longer errors — defaults to tier=worker.
// Tested separately in TestValidationNodeTierDefault.
{
name: "empty components",
def: &ServiceDef{