Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
12 changes: 4 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
#syntax=docker/dockerfile:1

ARG GO_VERSION=1.25.5
ARG GO_VERSION=1.25.11
ARG DOCS_FORMATS="md,yaml"

FROM --platform=${BUILDPLATFORM} golangci/golangci-lint:v2.8.0-alpine AS lint-base

FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION}-alpine AS base
RUN apk add --no-cache git rsync
RUN apk add --no-cache git
WORKDIR /app

# Docs generation and validation targets
Expand All @@ -20,10 +20,8 @@ FROM base AS docs-build
COPY --from=docs-gen /out/docsgen /usr/bin
ENV DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND="mcp"
ARG DOCS_FORMATS
RUN --mount=target=/context \
--mount=target=.,type=tmpfs <<EOT
RUN --mount=type=bind,target=.,rw <<EOT
set -e
rsync -a /context/. .
docsgen --formats "$DOCS_FORMATS" --source "docs/generator/reference"
mkdir /out
cp -r docs/generator/reference/* /out/
Expand All @@ -33,10 +31,8 @@ FROM scratch AS docs-update
COPY --from=docs-build /out /

FROM docs-build AS docs-validate
RUN --mount=target=/context \
--mount=target=.,type=tmpfs <<EOT
RUN --mount=type=bind,target=.,rw <<EOT
set -e
rsync -a /context/. .
git add -A
rm -rf docs/generator/reference/*
cp -rf /out/* ./docs/generator/reference/
Expand Down
4 changes: 2 additions & 2 deletions cmd/docker-mcp/commands/secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func listSecretCommand() *cobra.Command {
output := make([]secretListItem, 0, len(l))
for _, env := range l {
output = append(output, secretListItem{
Name: secret.StripNamespace(env.ID),
Name: secret.StripNamespace(env.ID.String()),
Provider: env.Provider,
})
}
Expand All @@ -107,7 +107,7 @@ func listSecretCommand() *cobra.Command {
}
var rows [][]string
for _, v := range l {
rows = append(rows, []string{v.ID, v.Provider})
rows = append(rows, []string{v.ID.String(), v.Provider})
}
formatting.PrettyPrintTable(rows, []int{40, 120})
return nil
Expand Down
100 changes: 25 additions & 75 deletions cmd/docker-mcp/secret-management/secret/secretsengine.go
Original file line number Diff line number Diff line change
@@ -1,112 +1,62 @@
package secret

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"os"
"path/filepath"
"time"
)

var ErrSecretNotFound = errors.New("secret not found")
"github.com/docker/secrets-engine/client"
"github.com/docker/secrets-engine/client/realms"
"github.com/docker/secrets-engine/x/api"
)

type Envelope struct {
ID string `json:"id"`
Value []byte `json:"value"`
Provider string `json:"provider"`
Metadata map[string]string `json:"metadata,omitempty"`
}
// ErrSecretNotFound is returned when a requested secret does not exist.
// It aliases the SDK's not-found error so callers can use errors.Is against either.
var ErrSecretNotFound = client.ErrSecretNotFound

func socketPath() string {
if dir, err := os.UserCacheDir(); err == nil {
return filepath.Join(dir, "docker-secrets-engine", "engine.sock")
}
return filepath.Join(os.TempDir(), "docker-secrets-engine", "engine.sock")
// newClient builds a Secrets Engine client pinned to the engine socket.
func newClient() (client.Client, error) {
return client.New(client.WithSocketPath(api.DefaultSocketPath()))
}

// newHTTPClient creates a fresh HTTP client for each request.
// This avoids connection state issues with Unix sockets that can cause hangs.
func newHTTPClient() *http.Client {
return &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return (&net.Dialer{}).DialContext(ctx, "unix", socketPath())
},
DisableKeepAlives: true,
},
}
}

func GetSecrets(ctx context.Context) ([]Envelope, error) {
pattern := `{"pattern": "docker/mcp/**"}`

req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://localhost/resolver.v1.ResolverService/GetSecrets", bytes.NewReader([]byte(pattern)))
// GetSecrets returns all secrets under the docker/mcp/** realm.
func GetSecrets(ctx context.Context) ([]client.Envelope, error) {
c, err := newClient()
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", "application/json")

client := newHTTPClient()

resp, err := client.Do(req)
envelopes, err := c.GetSecrets(ctx, realms.DockerMCPDefault)
if errors.Is(err, ErrSecretNotFound) {
return []client.Envelope{}, nil
}
if err != nil {
return nil, err
}
defer resp.Body.Close()

// No secrets found
if resp.StatusCode == http.StatusNotFound {
return []Envelope{}, nil
}

var secrets map[string][]Envelope
if err := json.NewDecoder(resp.Body).Decode(&secrets); err != nil {
return nil, fmt.Errorf("failed to unmarshal secrets response: %w", err)
}

return secrets["envelopes"], nil
return envelopes, nil
}

// GetSecret retrieves a single secret by its full key (e.g., "docker/mcp/oauth/github").
// Returns ErrSecretNotFound if the secret does not exist.
func GetSecret(ctx context.Context, key string) (*Envelope, error) {
pattern := fmt.Sprintf(`{"pattern": "%s"}`, key)

req, err := http.NewRequestWithContext(ctx, http.MethodPost,
"http://localhost/resolver.v1.ResolverService/GetSecrets",
bytes.NewReader([]byte(pattern)))
func GetSecret(ctx context.Context, key string) (*client.Envelope, error) {
pattern, err := client.ParsePattern(key)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", "application/json")

client := newHTTPClient()

resp, err := client.Do(req)
c, err := newClient()
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode == http.StatusNotFound {
envelopes, err := c.GetSecrets(ctx, pattern)
if errors.Is(err, ErrSecretNotFound) {
return nil, ErrSecretNotFound
}

var secrets map[string][]Envelope
if err := json.NewDecoder(resp.Body).Decode(&secrets); err != nil {
return nil, fmt.Errorf("failed to unmarshal secret response: %w", err)
if err != nil {
return nil, err
}

envelopes := secrets["envelopes"]
if len(envelopes) == 0 {
return nil, ErrSecretNotFound
}

return &envelopes[0], nil
}
2 changes: 1 addition & 1 deletion cmd/docker-mcp/server/secret_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func getConfiguredSecretNames(ctx context.Context) (map[string]struct{}, error)
configuredSecretNames := make(map[string]struct{})
for _, env := range envelopes {
// Extract base name from full ID using centralized namespace stripper
name := secret.StripNamespace(env.ID)
name := secret.StripNamespace(env.ID.String())
configuredSecretNames[name] = struct{}{}
}

Expand Down
52 changes: 28 additions & 24 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/docker/mcp-gateway

go 1.25.5
go 1.25.11

require (
github.com/Microsoft/go-winio v0.6.2
Expand All @@ -12,6 +12,8 @@ require (
github.com/docker/docker v28.3.3+incompatible
github.com/docker/docker-credential-helpers v0.9.3
github.com/docker/mcp-gateway-oauth-helpers v0.0.3
github.com/docker/secrets-engine/client v0.0.29
github.com/docker/secrets-engine/x v0.0.32-do.not.use
github.com/dop251/goja v0.0.0-20251008123653-cf18d89f3cf6
github.com/fsnotify/fsnotify v1.9.0
github.com/go-playground/validator/v10 v10.28.0
Expand All @@ -32,13 +34,13 @@ require (
github.com/spf13/cobra v1.10.1
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/otel v1.38.0
go.opentelemetry.io/otel/metric v1.38.0
go.opentelemetry.io/otel/sdk v1.38.0
go.opentelemetry.io/otel/sdk/metric v1.38.0
go.opentelemetry.io/otel/trace v1.38.0
go.opentelemetry.io/otel v1.40.0
go.opentelemetry.io/otel/metric v1.40.0
go.opentelemetry.io/otel/sdk v1.40.0
go.opentelemetry.io/otel/sdk/metric v1.40.0
go.opentelemetry.io/otel/trace v1.40.0
golang.org/x/oauth2 v0.34.0
golang.org/x/sync v0.19.0
golang.org/x/sync v0.20.0
gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.39.1
Expand All @@ -59,6 +61,8 @@ require (
)

require (
connectrpc.com/connect v1.19.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/go-openapi/swag/cmdutils v0.24.0 // indirect
github.com/go-openapi/swag/conv v0.24.0 // indirect
Expand Down Expand Up @@ -87,7 +91,7 @@ require (
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go-v2/service/ecr v1.44.2 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
Expand Down Expand Up @@ -130,7 +134,7 @@ require (
github.com/golang/snappy v1.0.0 // indirect
github.com/google/certificate-transparency-go v1.3.2 // indirect
github.com/google/uuid v1.6.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
github.com/in-toto/attestation v1.1.2 // indirect
github.com/in-toto/in-toto-golang v0.9.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
Expand Down Expand Up @@ -187,26 +191,26 @@ require (
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect
go.mongodb.org/mongo-driver v1.17.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/term v0.38.0 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a // indirect
google.golang.org/grpc v1.74.2 // indirect
google.golang.org/protobuf v1.36.8 // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/net v0.54.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/term v0.43.0 // indirect
golang.org/x/text v0.37.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/grpc v1.78.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gotest.tools/v3 v3.5.2 // indirect
k8s.io/client-go v0.33.1 // indirect
)
Expand Down
Loading
Loading