Skip to content
Open

MCP #77

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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ jobs:
steps:
- name: ⬇️ Checkout Code
uses: actions/checkout@v5
with:
fetch-depth: 0

- name: 🔧 Set up QEMU
uses: docker/setup-qemu-action@v3

- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: 🔑 Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: 📦 Install Mise
run: |
Expand Down
70 changes: 70 additions & 0 deletions .github/workflows/snapshot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: 🐳 Snapshot

on:
push:
branches:
- main
pull_request:
types: [ synchronize, opened, reopened, labeled ]

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read
packages: write

jobs:
snapshot:
if: >-
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
(github.event_name == 'pull_request' &&
contains(github.event.pull_request.labels.*.name, 'snapshot'))
runs-on: ubuntu-latest
steps:
- name: ⬇️ Checkout Code
uses: actions/checkout@v5
with:
fetch-depth: 0

- name: 🔧 Set up QEMU
uses: docker/setup-qemu-action@v3

- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: 🔑 Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: 📦 Install Mise
run: |
curl https://mise.run | sh
mise install

- name: 🏷️ Compute Docker tags
id: tags
run: |
short_sha="${GITHUB_SHA::7}"
if [ "${{ github.event_name }}" = "push" ]; then
echo "primary=main" >> "$GITHUB_OUTPUT"
echo "sha=main-${short_sha}" >> "$GITHUB_OUTPUT"
echo "extra=latest" >> "$GITHUB_OUTPUT"
else
slug="pr-${{ github.event.pull_request.number }}"
echo "primary=${slug}" >> "$GITHUB_OUTPUT"
echo "sha=${slug}-${short_sha}" >> "$GITHUB_OUTPUT"
echo "extra=" >> "$GITHUB_OUTPUT"
fi

- name: 🐳 Build & push Docker image
run: mise run docker-snapshot
env:
DOCKER_TAG_PRIMARY: ${{ steps.tags.outputs.primary }}
DOCKER_TAG_SHA: ${{ steps.tags.outputs.sha }}
DOCKER_TAG_EXTRA: ${{ steps.tags.outputs.extra }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
42 changes: 42 additions & 0 deletions .goreleaser.branch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
project_name: skpr

version: 2

builds:
- id: skpr-cli
main: ./cmd/skpr
binary: skpr
ldflags:
- -extldflags '-static' -X github.com/skpr/cli/cmd/skpr/version.GitVersion={{.Version}} -X github.com/skpr/cli/cmd/skpr/version.BuildDate={{time "2006-01-02"}}
env:
- CGO_ENABLED=0
goos: [ linux ]
goarch: [ amd64, arm64 ]
goamd64: [ v1 ]

- id: skpr-rsh
main: ./cmd/skpr-rsh
binary: skpr-rsh
ldflags:
- -extldflags '-static'
env:
- CGO_ENABLED=0
goos: [ linux ]
goarch: [ amd64, arm64 ]
goamd64: [ v1 ]

dockers_v2:
- id: skpr
ids:
- skpr-cli
- skpr-rsh
images:
- "ghcr.io/skpr/cli"
platforms:
- linux/amd64
- linux/arm64
tags:
- "{{ .Env.DOCKER_TAG_PRIMARY }}"
- "{{ .Env.DOCKER_TAG_SHA }}"
- "{{ .Env.DOCKER_TAG_EXTRA }}"
sbom: false
15 changes: 15 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,21 @@ release:
owner: skpr
name: cli

dockers_v2:
- id: skpr
ids:
- skpr-cli
- skpr-rsh
images:
- "ghcr.io/skpr/cli"
platforms:
- linux/amd64
- linux/arm64
tags:
- "{{ .Tag }}"
- "latest"
sbom: false

changelog:
use: github-native

Expand Down
5 changes: 5 additions & 0 deletions .mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,8 @@ depends = ["vendor"]
[tasks.snapshot-release]
description = "Create a snapshot release for local testing"
run = "goreleaser --snapshot --clean --verbose"

[tasks.docker-snapshot]
description = "Build and push a Docker image via goreleaser (no GitHub release)"
run = "goreleaser release --config=.goreleaser.branch.yml --clean --skip=validate"
depends = ["vendor"]
6 changes: 4 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
FROM alpine:3.21
FROM alpine:3.23

RUN apk --no-cache add bash ca-certificates git openssh-client curl rsync docker-cli jq yq
COPY skpr skpr-rsh /usr/local/bin/

ARG TARGETPLATFORM
COPY $TARGETPLATFORM/skpr $TARGETPLATFORM/skpr-rsh /usr/local/bin/

CMD ["skpr"]
2 changes: 2 additions & 0 deletions cmd/skpr/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/skpr/cli/cmd/skpr/login"
"github.com/skpr/cli/cmd/skpr/logout"
"github.com/skpr/cli/cmd/skpr/logs"
mcpcmd "github.com/skpr/cli/cmd/skpr/mcp"
"github.com/skpr/cli/cmd/skpr/mysql"
pkg "github.com/skpr/cli/cmd/skpr/package"
"github.com/skpr/cli/cmd/skpr/project"
Expand Down Expand Up @@ -107,6 +108,7 @@ func main() {
cmd.AddCommand(login.NewCommand())
cmd.AddCommand(logout.NewCommand())
cmd.AddCommand(logs.NewCommand())
cmd.AddCommand(mcpcmd.NewCommand(featureFlags.DockerClient))
cmd.AddCommand(mysql.NewCommand(featureFlags.DockerClient))
cmd.AddCommand(pkg.NewCommand(featureFlags.DockerClient))
cmd.AddCommand(project.NewCommand())
Expand Down
115 changes: 115 additions & 0 deletions cmd/skpr/mcp/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package mcp

import (
"github.com/spf13/cobra"

"github.com/skpr/cli/containers/docker"
skprcommand "github.com/skpr/cli/internal/command"
v1mcp "github.com/skpr/cli/internal/command/mcp"
)

var (
cmdLong = `Run a Model Context Protocol (MCP) server.

The server exposes a subset of skpr's functionality as MCP tools so that it
can be wired into any MCP-compatible client (Claude Desktop, opencode, etc.).

Available tools
list_environments List all environments for the current project.
get_environment Get detailed information about a specific environment.
mysql_image_list List database images available for an environment.
mysql_image_pull Pull a database image to the local Docker daemon.
version Return the CLI client and server versions.

Stdio mode (default)
Reads JSON-RPC from stdin and writes responses to stdout. Use this when
an MCP client spawns skpr as a child process.

Example client configuration (opencode / claude_desktop_config.json):

"skpr": {
"type": "stdio",
"command": "skpr",
"args": ["mcp"]
}

HTTP mode (--http)
Starts a Streamable HTTP server on the given address. Use this when running
skpr as a sidecar container or when multiple clients need to share one server.

Example — listen on all interfaces, port 8080:
skpr mcp --http :8080

Example — localhost only (recommended for local development):
skpr mcp --http 127.0.0.1:8080

Example client configuration for HTTP:
"skpr": {
"type": "http",
"url": "http://localhost:8080/"
}

Docker sidecar example:
docker run --rm \
-v ~/.config/skpr:/root/.config/skpr:ro \
-p 8080:8080 \
skpr mcp --http :8080

Kubernetes sidecar container spec:
- name: skpr-mcp
image: ghcr.io/skpr/cli:latest
args: ["mcp", "--http", ":8080"]
ports:
- containerPort: 8080
volumeMounts:
- name: skpr-config
mountPath: /root/.config/skpr
readOnly: true

Health probe (liveness / readiness):
GET /healthz → 200 OK body: ok

SECURITY NOTE
HTTP mode exposes your skpr credentials to anyone who can reach the
listening port. There is no built-in authentication in this release.
Bind to localhost or a private network interface, and use your
infrastructure's network controls to restrict access.`

// GitVersion is overridden at build time via ldflags (shared with the
// version command).
GitVersion string
// BuildDate is overridden at build time via ldflags.
BuildDate string
)

// NewCommand creates the cobra.Command for 'skpr mcp'.
func NewCommand(clientId docker.DockerClientId) *cobra.Command {
command := &v1mcp.Command{
ClientId: clientId,
GitVersion: GitVersion,
BuildDate: BuildDate,
}

cmd := &cobra.Command{
Use: "mcp",
Args: cobra.NoArgs,
DisableFlagsInUseLine: true,
Short: "Run a Model Context Protocol server",
Long: cmdLong,
GroupID: skprcommand.GroupOther,
RunE: func(cmd *cobra.Command, _ []string) error {
return command.Run(cmd.Context())
},
}

cmd.Flags().StringVar(&command.HTTPAddr, "http", "",
`Listen address for HTTP transport (e.g. ":8080" or "127.0.0.1:8080"). Empty uses stdio.`)
cmd.Flags().StringVar(&command.HTTPPath, "path", "/",
"URL path to mount the MCP handler on (HTTP mode only).")
cmd.Flags().BoolVar(&command.HTTPStateless, "stateless", false,
"Disable session tracking (HTTP mode only). Useful behind a stateless load balancer.")
cmd.Flags().BoolVar(&command.HTTPAllowCrossOriginHosts, "allow-cross-origin-hosts", false,
"Disable DNS-rebinding protection (HTTP mode only). Only use if you understand the security implications.")

return cmd
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ require (
github.com/moby/go-archive v0.2.0
github.com/moby/moby/api v1.54.2
github.com/moby/patternmatcher v0.6.1
github.com/modelcontextprotocol/go-sdk v1.6.1
github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0
github.com/pkg/errors v0.9.1
github.com/skpr/api v1.6.0
Expand Down Expand Up @@ -83,6 +84,7 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/jsonschema-go v0.4.3 // indirect
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jwalton/go-supportscolor v1.2.0 // indirect
Expand Down Expand Up @@ -111,9 +113,12 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/segmentio/asm v1.1.3 // indirect
github.com/segmentio/encoding v0.5.4 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
Expand Down
14 changes: 14 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,14 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
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/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
Expand Down Expand Up @@ -181,6 +185,8 @@ github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modelcontextprotocol/go-sdk v1.6.1 h1:0zOSupjKUxPKSocPT1Wtago+mUHU2/uZ4xSOY0FGReU=
github.com/modelcontextprotocol/go-sdk v1.6.1/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ=
github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
Expand Down Expand Up @@ -215,6 +221,10 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skpr/api v1.6.0 h1:LzbNm97wXvnWAcm+iyV1r36YKGMFWNn65y+EHhBxgqo=
Expand All @@ -236,6 +246,8 @@ github.com/tcnksm/go-input v0.0.0-20180404061846-548a7d7a8ee8 h1:RB0v+/pc8oMzPsN
github.com/tcnksm/go-input v0.0.0-20180404061846-548a7d7a8ee8/go.mod h1:IlWNj9v/13q7xFbaK4mbyzMNwrZLaWSHx/aibKIZuIg=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
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=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
Expand Down Expand Up @@ -284,6 +296,8 @@ golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4=
Expand Down
Loading
Loading