Repository: superfly/flyctl
Pull request #4309 (2025-04-11 09:35:19)

Bump github.com/getsentry/sentry-go from 0.31.1 to 0.32.0
Bumps [github.com/getsentry/sentry-go](https://github.com/getsentry/sentry-go) from 0.31.1 to 0.32.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/getsentry/sentry-go/releases">github.com/getsentry/sentry-go's releases</a>.</em></p>
<blockquote>
<h2>0.32.0</h2>
<p>The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.32.0.</p>
<h3>Breaking Changes</h3>
<ul>
<li>Bump the minimum Go version to 1.22. The supported versions are 1.22, 1.23 and 1.24. (<a href="https://redirect.github.com/getsentry/sentry-go/issues/967">#967</a>)</li>
<li>Setting any values on <code>span.Extra</code> has no effect anymore. Use <code>SetData(name string, value interface{})</code> instead. (<a href="https://redirect.github.com/getsentry/sentry-go/pull/864">#864</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li>Add a <code>MockTransport</code> and <code>MockScope</code>. (<a href="https://redirect.github.com/getsentry/sentry-go/pull/972">#972</a>)</li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li>Fix writing <code>*http.Request</code> in the Logrus JSONFormatter. (<a href="https://redirect.github.com/getsentry/sentry-go/issues/955">#955</a>)</li>
</ul>
<h3>Misc</h3>
<ul>
<li>Transaction <code>data</code> attributes are now seralized as trace context data attributes, allowing you to query these attributes in the <a href="https://docs.sentry.io/product/explore/traces/">Trace Explorer</a>.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/getsentry/sentry-go/blob/master/CHANGELOG.md">github.com/getsentry/sentry-go's changelog</a>.</em></p>
<blockquote>
<h2>0.32.0</h2>
<p>The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.32.0.</p>
<h3>Breaking Changes</h3>
<ul>
<li>Bump the minimum Go version to 1.22. The supported versions are 1.22, 1.23 and 1.24. (<a href="https://redirect.github.com/getsentry/sentry-go/issues/967">#967</a>)</li>
<li>Setting any values on <code>span.Extra</code> has no effect anymore. Use <code>SetData(name string, value interface{})</code> instead. (<a href="https://redirect.github.com/getsentry/sentry-go/pull/864">#864</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li>Add a <code>MockTransport</code> and <code>MockScope</code>. (<a href="https://redirect.github.com/getsentry/sentry-go/pull/972">#972</a>)</li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li>Fix writing <code>*http.Request</code> in the Logrus JSONFormatter. (<a href="https://redirect.github.com/getsentry/sentry-go/issues/955">#955</a>)</li>
</ul>
<h3>Misc</h3>
<ul>
<li>Transaction <code>data</code> attributes are now seralized as trace context data attributes, allowing you to query these attributes in the <a href="https://docs.sentry.io/product/explore/traces/">Trace Explorer</a>.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/getsentry/sentry-go/commit/530f74d352a3533855e84572c8d0f4e790ae674b"><code>530f74d</code></a> release: 0.32.0</li>
<li><a href="https://github.com/getsentry/sentry-go/commit/e068944300b5225ae481b0bd8f57bfd8e6441bae"><code>e068944</code></a> Prepare 0.32.0 (<a href="https://redirect.github.com/getsentry/sentry-go/issues/992">#992</a>)</li>
<li><a href="https://github.com/getsentry/sentry-go/commit/32c062f0707339f391efc62a05e11e5e3f0b0780"><code>32c062f</code></a> Add <code>transaction.data</code> to <code>contexts.trace.data</code> (<a href="https://redirect.github.com/getsentry/sentry-go/issues/864">#864</a>)</li>
<li><a href="https://github.com/getsentry/sentry-go/commit/6019770393710ba5afe949c459289635e73a55b1"><code>6019770</code></a> Fix lint issues (<a href="https://redirect.github.com/getsentry/sentry-go/issues/981">#981</a>)</li>
<li><a href="https://github.com/getsentry/sentry-go/commit/cfbfc6b38a42c0254ff00e0f8244d04f785c0f9c"><code>cfbfc6b</code></a> Expose MockTransport, MockScope in root sentry package (<a href="https://redirect.github.com/getsentry/sentry-go/issues/972">#972</a>)</li>
<li><a href="https://github.com/getsentry/sentry-go/commit/a4e0ea8a065445c29d25effb3d93e16170bb2931"><code>a4e0ea8</code></a> Remove github token in lint (<a href="https://redirect.github.com/getsentry/sentry-go/issues/982">#982</a>)</li>
<li><a href="https://github.com/getsentry/sentry-go/commit/031ec47dc927e3fb737fa818285ccf3f46b47c7f"><code>031ec47</code></a> build(deps): bump golangci/golangci-lint-action from 6.2.0 to 6.5.0 (<a href="https://redirect.github.com/getsentry/sentry-go/issues/975">#975</a>)</li>
<li><a href="https://github.com/getsentry/sentry-go/commit/bdbb6be4a0bae2f225b22b5dca9f1c7af53c06f3"><code>bdbb6be</code></a> build(deps): bump codecov/codecov-action from 5.3.1 to 5.4.0 (<a href="https://redirect.github.com/getsentry/sentry-go/issues/974">#974</a>)</li>
<li><a href="https://github.com/getsentry/sentry-go/commit/3d8d0e14fd1ddff49af13cff94931114dec68c67"><code>3d8d0e1</code></a> build(deps): bump actions/create-github-app-token from 1.11.2 to 1.11.5 (<a href="https://redirect.github.com/getsentry/sentry-go/issues/973">#973</a>)</li>
<li><a href="https://github.com/getsentry/sentry-go/commit/e56ac3010f8c48bbb32839ff8a7e751f420c361f"><code>e56ac30</code></a> build(deps): bump codecov/codecov-action from 5.1.2 to 5.3.1 (<a href="https://redirect.github.com/getsentry/sentry-go/issues/962">#962</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/getsentry/sentry-go/compare/v0.31.1...v0.32.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/getsentry/sentry-go&package-manager=go_modules&previous-version=0.31.1&new-version=0.32.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/go.mod b/go.mod
index 09461cdf4..127bc6baa 100644
--- a/go.mod
+++ b/go.mod
@@ -32,7 +32,7 @@ require (
 	github.com/docker/go-units v0.5.0
 	github.com/dustin/go-humanize v1.0.1
 	github.com/ejcx/sshcert v1.1.0
-	github.com/getsentry/sentry-go v0.31.1
+	github.com/getsentry/sentry-go v0.32.0
 	github.com/go-kit/log v0.2.1
 	github.com/go-logr/logr v1.4.2
 	github.com/gofrs/flock v0.12.1
@@ -65,7 +65,7 @@ require (
 	github.com/pkg/errors v0.9.1
 	github.com/pkg/sftp v1.13.9
 	github.com/prometheus/blackbox_exporter v0.25.0
-	github.com/prometheus/client_golang v1.20.5
+	github.com/prometheus/client_golang v1.22.0
 	github.com/prometheus/client_model v0.6.1
 	github.com/r3labs/diff v1.1.0
 	github.com/samber/lo v1.49.1
@@ -102,6 +102,8 @@ require (
 	gopkg.in/yaml.v3 v3.0.1
 )
 
+require github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
+
 require (
 	connectrpc.com/connect v1.16.1 // indirect
 	dario.cat/mergo v1.0.1 // indirect
@@ -212,6 +214,7 @@ require (
 	github.com/launchdarkly/go-jsonstream/v3 v3.0.0 // indirect
 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
+	github.com/mark3labs/mcp-go v0.19.0
 	github.com/mattn/go-runewidth v0.0.16 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
 	github.com/mitchellh/ioprogress v0.0.0-20180201004757-6a23b12fa88e // indirect
@@ -233,7 +236,7 @@ require (
 	github.com/pjbgf/sha1cd v0.3.0 // indirect
 	github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
-	github.com/prometheus/common v0.55.0 // indirect
+	github.com/prometheus/common v0.62.0 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
diff --git a/go.sum b/go.sum
index 329f50efb..bef1c062b 100644
--- a/go.sum
+++ b/go.sum
@@ -282,8 +282,8 @@ github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeekl
 github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04=
 github.com/gdamore/tcell/v2 v2.8.0 h1:IDclow1j6kKpU/gOhjmc+7Pj5Dxnukb74pfKN4Cxrfg=
 github.com/gdamore/tcell/v2 v2.8.0/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw=
-github.com/getsentry/sentry-go v0.31.1 h1:ELVc0h7gwyhnXHDouXkhqTFSO5oslsRDk0++eyE0KJ4=
-github.com/getsentry/sentry-go v0.31.1/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY=
+github.com/getsentry/sentry-go v0.32.0 h1:YKs+//QmwE3DcYtfKRH8/KyOOF/I6Qnx7qYGNHCGmCY=
+github.com/getsentry/sentry-go v0.32.0/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY=
 github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
 github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
 github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
@@ -432,6 +432,8 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
 github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mark3labs/mcp-go v0.19.0 h1:cYKBPFD+fge273/TV6f5+TZYBSTnxV6GCJAO08D2wvA=
+github.com/mark3labs/mcp-go v0.19.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=
 github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
 github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
 github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
@@ -542,16 +544,16 @@ github.com/prometheus/blackbox_exporter v0.25.0/go.mod h1:SpTDn8xW1XOstBQ1uVgw54
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
 github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
-github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
-github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
+github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
+github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
 github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
-github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
-github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
+github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
+github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
@@ -670,6 +672,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo
 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
 github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
+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=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
diff --git a/internal/cli/cli.go b/internal/cli/cli.go
index 2132a61e4..2c752c94e 100644
--- a/internal/cli/cli.go
+++ b/internal/cli/cli.go
@@ -67,10 +67,11 @@ func Run(ctx context.Context, io *iostreams.IOStreams, args ...string) int {
 	cmd.SetErr(io.ErrOut)
 
 	// Special case for the launch command, support `flyctl launch args -- [subargs]`
+	// and `flyctl mcp wrap --mcp script -- [subargs]`
 	// Where the arguments after `--` are passed to the scanner/dockerfile generator.
 	// This isn't supported natively by cobra, so we have to manually split the args
 	// See: https://github.com/spf13/cobra/issues/739
-	if len(args) > 0 && args[0] == "launch" {
+	if (len(args) > 0 && args[0] == "launch") || (len(args) > 2 && args[0] == "mcp" && args[1] == "wrap") {
 		index := slices.Index(args, "--")
 		if index >= 0 {
 			ctx = flag.WithExtraArgs(ctx, args[index+1:])
diff --git a/internal/command/deploy/plan.go b/internal/command/deploy/plan.go
index c2fa84098..03f569ffb 100644
--- a/internal/command/deploy/plan.go
+++ b/internal/command/deploy/plan.go
@@ -482,6 +482,7 @@ func (md *machineDeployment) releaseLeases(ctx context.Context, machineTuples []
 				sl.LogStatus(statuslogger.StatusFailure, fmt.Sprintf("Failed to clear lease for %s: %v", machine.ID, err))
 				return err
 			}
+			machine.LeaseNonce = ""
 
 			sl.LogStatus(statuslogger.StatusSuccess, fmt.Sprintf("Cleared lease for %s", machine.ID))
 			return nil
diff --git a/internal/command/mcp/mcp.go b/internal/command/mcp/mcp.go
new file mode 100644
index 000000000..67f2ef3d2
--- /dev/null
+++ b/internal/command/mcp/mcp.go
@@ -0,0 +1,25 @@
+package mcp
+
+import (
+	"github.com/spf13/cobra"
+	"github.com/superfly/flyctl/internal/command"
+)
+
+func New() *cobra.Command {
+	const (
+		short = `flyctl Model Context Protocol.`
+
+		long = short + "\n"
+	)
+
+	cmd := command.New("mcp", short, long, nil)
+	// cmd.Hidden = true
+
+	cmd.AddCommand(
+		NewProxy(),
+		newServer(),
+		NewWrap(),
+	)
+
+	return cmd
+}
diff --git a/internal/command/mcp/proxy.go b/internal/command/mcp/proxy.go
new file mode 100644
index 000000000..8bb8d710e
--- /dev/null
+++ b/internal/command/mcp/proxy.go
@@ -0,0 +1,198 @@
+package mcp
+
+import (
+	"bufio"
+	"bytes"
+	"context"
+	"fmt"
+	"io"
+	"log"
+	"net/http"
+	"os"
+	"os/signal"
+	"strings"
+	"syscall"
+	"time"
+
+	"github.com/spf13/cobra"
+	"github.com/superfly/flyctl/internal/command"
+	"github.com/superfly/flyctl/internal/flag"
+)
+
+func NewProxy() *cobra.Command {
+	const (
+		short = "[experimental] Start an MCP proxy client"
+		long  = short + "\n"
+		usage = "proxy"
+	)
+
+	cmd := command.New(usage, short, long, runProxy)
+	cmd.Args = cobra.ExactArgs(0)
+
+	flag.Add(cmd,
+		flag.String{
+			Name:        "url",
+			Description: "URL of the MCP wrapper server",
+		},
+		flag.String{
+			Name:        "user",
+			Description: "[optional] User to authenticate with",
+			Shorthand:   "u",
+		},
+		flag.String{
+			Name:        "password",
+			Description: "[optional] Password to authenticate with",
+			Shorthand:   "p",
+		},
+	)
+
+	return cmd
+}
+
+func runProxy(ctx context.Context) error {
+	// Validate inputs
+	if flag.GetString(ctx, "url") == "" {
+		log.Fatal("--url is required")
+	}
+
+	// Configure logging to go to stderr only
+	log.SetOutput(os.Stderr)
+
+	// Start the HTTP client
+	go func() {
+		start := time.Now()
+		for {
+			getFromServer(ctx)
+
+			// Wait a minimum of 10 seconds before the next request
+			elapsed := time.Since(start)
+			if elapsed < 10*time.Second {
+				time.Sleep(10*time.Second - elapsed)
+			}
+			start = time.Now()
+		}
+	}()
+
+	// Start processing stdin
+	if err := processStdin(ctx); err != nil {
+		log.Fatalf("Error processing stdin: %v", err)
+	}
+
+	return nil
+}
+
+// ProcessStdin reads messages from stdin and forwards them to the server
+func processStdin(ctx context.Context) error {
+	stp := make(chan os.Signal, 1)
+	signal.Notify(stp, syscall.SIGINT, syscall.SIGTERM)
+	go func() {
+		<-stp
+		os.Exit(0)
+	}()
+
+	scanner := bufio.NewScanner(os.Stdin)
+	for scanner.Scan() {
+		line := scanner.Text() + "\n"
+
+		// Skip empty lines
+		if strings.TrimSpace(line) == "" {
+			continue
+		}
+
+		// Forward raw message to server
+		err := sendToServer(ctx, line)
+		if err != nil {
+			// Log error but continue processing
+			log.Printf("Error sending message to server: %v", err)
+			// We could format an error message here, but since we're operating at the raw string level,
+			// we'll return a generic error JSON
+			errMsg := fmt.Sprintf(`{"type":"error","content":"Failed to send to server: %v"}`, err)
+			fmt.Fprintln(os.Stdout, errMsg)
+			continue
+		}
+	}
+
+	if err := scanner.Err(); err != nil {
+		return fmt.Errorf("error reading from stdin: %w", err)
+	}
+
+	return nil
+}
+
+func getFromServer(ctx context.Context) error {
+	url := flag.GetString(ctx, "url")
+
+	// Create HTTP request
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		return fmt.Errorf("error creating request: %w", err)
+	}
+	req.Header.Set("User-Agent", "mcp-bridge-client")
+	req.Header.Set("Accept", "application/json")
+
+	// Set basic authentication if user is provided
+	if flag.GetString(ctx, "user") != "" {
+		req.SetBasicAuth(flag.GetString(ctx, "user"), flag.GetString(ctx, "password"))
+	}
+
+	// Send request
+	client := &http.Client{}
+	resp, err := client.Do(req)
+	if err != nil {
+		return fmt.Errorf("error sending request: %w", err)
+	}
+	defer resp.Body.Close()
+
+	// Check response status
+	if resp.StatusCode != http.StatusOK {
+		return fmt.Errorf("server returned error: %s (status %d)", resp.Status, resp.StatusCode)
+	}
+
+	// Stream response body to stdout
+	if _, err := io.Copy(os.Stdout, resp.Body); err != nil {
+		return fmt.Errorf("error streaming response to stdout: %w", err)
+	}
+
+	return nil
+}
+
+// SendToServer sends a raw message to the server and returns the raw response
+func sendToServer(ctx context.Context, message string) error {
+	url := flag.GetString(ctx, "url")
+
+	// Create HTTP request with raw message
+	req, err := http.NewRequest("POST", url, bytes.NewBufferString(message))
+	if err != nil {
+		return fmt.Errorf("error creating request: %w", err)
+	}
+	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("User-Agent", "mcp-bridge-client")
+	req.Header.Set("Accept", "application/json, text/event-stream")
+
+	// Set basic authentication if user is provided
+	if flag.GetString(ctx, "user") != "" {
+		req.SetBasicAuth(flag.GetString(ctx, "user"), flag.GetString(ctx, "password"))
+	}
+
+	// Send request
+	client := &http.Client{}
+	resp, err := client.Do(req)
+	if err != nil {
+		return fmt.Errorf("error sending request: %w", err)
+	}
+	defer resp.Body.Close()
+
+	// Check response status
+	if resp.StatusCode != http.StatusAccepted {
+		// Read response body
+		body, err := io.ReadAll(resp.Body)
+		if err != nil {
+			return fmt.Errorf("error reading response: %w", err)
+		}
+
+		return fmt.Errorf("server returned error: %s (status %d)", body, resp.StatusCode)
+	}
+
+	// Request was accepted
+	return nil
+}
diff --git a/internal/command/mcp/server.go b/internal/command/mcp/server.go
new file mode 100644
index 000000000..785922730
--- /dev/null
+++ b/internal/command/mcp/server.go
@@ -0,0 +1,117 @@
+package mcp
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"os/exec"
+
+	"github.com/mark3labs/mcp-go/mcp"
+	"github.com/mark3labs/mcp-go/server"
+	"github.com/spf13/cobra"
+	"github.com/superfly/flyctl/internal/command"
+)
+
+func newServer() *cobra.Command {
+	const (
+		short = "[experimental] Start a flyctl MCP server"
+		long  = short + "\n"
+		usage = "server"
+	)
+
+	cmd := command.New(usage, short, long, runServer)
+	cmd.Args = cobra.ExactArgs(0)
+
+	return cmd
+}
+
+func runServer(ctx context.Context) error {
+	flyctl, err := os.Executable()
+	if err != nil {
+		return fmt.Errorf("failed to find executable: %w", err)
+	}
+
+	// Create MCP server
+	srv := server.NewMCPServer(
+		"FlyMCP 🚀",
+		"1.0.0",
+	)
+
+	// Register logs capability
+	srv.AddTool(mcp.NewTool("fly-logs",
+		mcp.WithDescription("Get logs for a Fly.io app or specific machine"),
+		mcp.WithString("app",
+			mcp.Required(),
+			mcp.Description("Name of the app"),
+		),
+		mcp.WithString("machine",
+			mcp.Description("Specific machine ID"),
+		),
+	), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+		app, ok := request.Params.Arguments["app"].(string)
+		if !ok {
+			return nil, fmt.Errorf("app parameter is required and must be a string")
+		}
+
+		var machine string
+		if machineVal, ok := request.Params.Arguments["machine"]; ok && machineVal != nil {
+			if machine, ok = machineVal.(string); !ok {
+				return nil, fmt.Errorf("machine parameter must be a string")
+			}
+		}
+
+		args := []string{"logs", "--no-tail"}
+		if app != "" {
+			args = append(args, "-a", app)
+		}
+		if machine != "" {
+			args = append(args, "--machine", machine)
+		}
+
+		fmt.Fprintf(os.Stderr, "Executing flyctl command: %v\n", args)
+		cmd := exec.Command(flyctl, args...)
+		output, err := cmd.CombinedOutput()
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "Error executing flyctl: %v\nOutput: %s\n", err, string(output))
+			return nil, fmt.Errorf("failed to get logs: %v\nOutput: %s", err, string(output))
+		}
+
+		return mcp.NewToolResultText(string(output)), nil
+	})
+
+	// Register status capability
+	srv.AddTool(mcp.NewTool("fly-status",
+		mcp.WithDescription("Get status of a Fly.io app"),
+		mcp.WithString("app",
+			mcp.Required(),
+			mcp.Description("Name of the app"),
+		),
+	), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+		app, ok := request.Params.Arguments["app"].(string)
+		if !ok {
+			return nil, fmt.Errorf("app parameter is required and must be a string")
+		}
+
+		args := []string{"status", "--json"}
+		if app != "" {
+			args = append(args, "-a", app)
+		}
+
+		fmt.Fprintf(os.Stderr, "Executing flyctl command: %v\n", args)
+		cmd := exec.Command(flyctl, args...)
+		output, err := cmd.CombinedOutput()
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "Error executing flyctl: %v\nOutput: %s\n", err, string(output))
+			return nil, fmt.Errorf("failed to get status: %v\nOutput: %s", err, string(output))
+		}
+
+		return mcp.NewToolResultText(string(output)), nil
+	})
+
+	fmt.Fprintf(os.Stderr, "Starting MCP server...\n")
+	if err := server.ServeStdio(srv); err != nil {
+		return fmt.Errorf("Server error: %v\n", err)
+	}
+
+	return nil
+}
diff --git a/internal/command/mcp/wrap.go b/internal/command/mcp/wrap.go
new file mode 100644
index 000000000..82a32c22d
--- /dev/null
+++ b/internal/command/mcp/wrap.go
@@ -0,0 +1,270 @@
+package mcp
+
+import (
+	"bufio"
+	"context"
+	"fmt"
+	"io"
+	"log"
+	"net/http"
+	"os"
+	"os/exec"
+	"os/signal"
+	"sync"
+	"syscall"
+
+	"github.com/spf13/cobra"
+	"github.com/superfly/flyctl/internal/command"
+	"github.com/superfly/flyctl/internal/flag"
+)
+
+// This program is a simple HTTP server that forwards POST requests to an MCP stdio program,
+// and streams the program's output back to the client. It uses Server-Sent Events (SSE)
+// to push updates from the server to the client.
+//
+// It is a streamlined version of the MCP proxy server, focusing on a single session:
+// See https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http
+
+// Server handles HTTP requests and communicates with the remote program
+type Server struct {
+	port     int
+	mcp      string
+	user     string
+	password string
+	cmd      *exec.Cmd
+	args     []string
+	stdin    io.WriteCloser
+	stdout   io.ReadCloser
+	mutex    sync.Mutex
+	client   chan string
+}
+
+func NewWrap() *cobra.Command {
+	const (
+		short = "[experimental] Wrap an MCP stdio program"
+		long  = short + `. Options passed after double dashes ("--") will be passed to the MCP program. If user is specified, HTTP authentication will be required.` + "\n"
+		usage = "wrap"
+	)
+
+	cmd := command.New(usage, short, long, runWrap)
+	cmd.Args = cobra.ExactArgs(0)
+
+	flag.Add(cmd,
+		flag.Int{
+			Name:        "port",
+			Description: "[optional] Port to listen on.  Defaults to 8080.",
+			Default:     8080,
+			Shorthand:   "p",
+		},
+		flag.String{
+			Name:        "mcp",
+			Description: "[required] Path to the stdio MCP program to be wrapped.",
+			Shorthand:   "m",
+		},
+		flag.String{
+			Name:        "user",
+			Description: "[optional] User to authenticate with. Defaults to the value of the FLY_MCP_USER environment variable.",
+		},
+		flag.String{
+			Name:        "password",
+			Description: "[optional] Password to authenticate with. Defaults to the value of the FLY_MCP_PASSWORD environment variable.",
+		},
+	)
+
+	return cmd
+}
+
+func runWrap(ctx context.Context) error {
+	// Create server
+	server := &Server{
+		port:     flag.GetInt(ctx, "port"),
+		user:     flag.GetString(ctx, "user"),
+		password: flag.GetString(ctx, "password"),
+		mcp:      flag.GetString(ctx, "mcp"),
+		args:     flag.ExtraArgsFromContext(ctx),
+		client:   nil,
+	}
+
+	// if user and password are not set, try to get them from environment variables
+	if server.user == "" {
+		server.user = os.Getenv("FLY_MCP_USER")
+	}
+
+	if server.password == "" {
+		server.password = os.Getenv("FLY_MCP_PASSWORD")
+	}
+
+	// Validate inputs
+	if server.mcp == "" {
+		log.Fatal("--mcp is required")
+	}
+
+	// Start the program
+	if err := server.StartProgram(); err != nil {
+		log.Fatalf("Error starting program: %v", err)
+	}
+	defer server.StopProgram()
+
+	// Start reading from the program's stdout
+	go server.ReadFromProgram()
+
+	// Set up HTTP server
+	http.HandleFunc("/", server.HandleHTTPRequest)
+	address := fmt.Sprintf(":%d", server.port)
+
+	log.Printf("Starting server on %s, forwarding to stdio MCP: %s", address, server.mcp)
+	if err := http.ListenAndServe(address, nil); err != nil {
+		log.Fatalf("Error starting server: %v", err)
+	}
+
+	return nil
+}
+
+// StartProgram starts the remote program and connects to its stdin/stdout
+func (s *Server) StartProgram() error {
+	cmd := exec.Command(s.mcp, s.args...)
+
+	// Get stdin pipe
+	stdin, err := cmd.StdinPipe()
+	if err != nil {
+		return fmt.Errorf("error getting stdin pipe: %w", err)
+	}
+	s.stdin = stdin
+
+	// Get stdout pipe
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		return fmt.Errorf("error getting stdout pipe: %w", err)
+	}
+	s.stdout = stdout
+
+	// Redirect stderr to our stderr
+	cmd.Stderr = os.Stderr
+
+	// Start the command
+	if err := cmd.Start(); err != nil {
+		return fmt.Errorf("error starting program: %w", err)
+	}
+
+	s.cmd = cmd
+
+	// Monitor program exit
+	go func() {
+		err := cmd.Wait()
+		if err != nil {
+			log.Printf("Program exited with error: %v", err)
+		} else {
+			log.Println("Program exited normally")
+		}
+	}()
+
+	return nil
+}
+
+// StopProgram stops the remote program
+func (s *Server) StopProgram() {
+	if s.cmd != nil && s.cmd.Process != nil {
+		log.Println("Stopping program")
+		if err := s.cmd.Process.Kill(); err != nil {
+			log.Printf("Error killing program: %v", err)
+		}
+	}
+}
+
+// ReadFromProgram continuously reads from the program's stdout
+func (s *Server) ReadFromProgram() {
+	stp := make(chan os.Signal, 1)
+	signal.Notify(stp, syscall.SIGINT, syscall.SIGTERM)
+	go func() {
+		<-stp
+		s.StopProgram()
+		os.Exit(0)
+	}()
+
+	scanner := bufio.NewScanner(s.stdout)
+	for scanner.Scan() {
+		line := scanner.Text() + "\n"
+
+		// Forward message to waiting client
+		s.mutex.Lock()
+		if s.client != nil {
+			s.client <- line
+		} else {
+			log.Printf("No client waiting")
+		}
+		s.mutex.Unlock()
+	}
+
+	if err := scanner.Err(); err != nil {
+		log.Printf("Error reading from program: %v", err)
+	} else {
+		log.Println("Program output stream closed")
+	}
+
+	// Close stdin to signal EOF to the program
+	if err := s.stdin.Close(); err != nil {
+		log.Printf("Error closing stdin: %v", err)
+	}
+	// Close stdout to signal EOF to the program
+	if err := s.stdout.Close(); err != nil {
+		log.Printf("Error closing stdout: %v", err)
+	}
+}
+
+// HandleHTTPRequest handles incoming HTTP requests
+func (s *Server) HandleHTTPRequest(w http.ResponseWriter, r *http.Request) {
+	if s.user != "" {
+		// Check for basic authentication
+		user, password, ok := r.BasicAuth()
+		if !ok || user != s.user || password != s.password {
+			http.Error(w, "Unauthorized", http.StatusUnauthorized)
+			return
+		}
+	}
+
+	// Handle GET requests
+	if r.Method == http.MethodGet {
+		// Set headers for SSE
+		w.Header().Set("Content-Type", "text/event-stream")
+		w.Header().Set("Cache-Control", "no-cache")
+		w.Header().Set("Connection", "keep-alive")
+		w.WriteHeader(http.StatusOK)
+
+		// Create channel for response
+		responseCh := make(chan string, 1)
+		s.mutex.Lock()
+		s.client = responseCh
+		s.mutex.Unlock()
+
+		// Stream responses to the client
+		for {
+			select {
+			case response := <-responseCh:
+				w.Write([]byte(response))
+				w.(http.Flusher).Flush() // Flush the response to the client
+			case <-r.Context().Done():
+				// Request was cancelled
+				s.mutex.Lock()
+				s.client = nil
+				s.mutex.Unlock()
+				return
+			}
+		}
+
+	} else if r.Method == http.MethodPost {
+		// Stream request body to program's stdin
+		if _, err := io.Copy(s.stdin, r.Body); err != nil {
+			log.Printf("Error writing to program: %v", err)
+			http.Error(w, fmt.Sprintf("Error writing to program: %v", err), http.StatusInternalServerError)
+		} else {
+			// Successfully wrote to program
+			w.WriteHeader(http.StatusAccepted)
+		}
+		defer r.Body.Close()
+
+	} else {
+		// Method not allowed
+		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+		return
+	}
+}
diff --git a/internal/command/mpg/connect.go b/internal/command/mpg/connect.go
index 01d3fee22..07617b7aa 100644
--- a/internal/command/mpg/connect.go
+++ b/internal/command/mpg/connect.go
@@ -53,7 +53,7 @@ func runConnect(ctx context.Context) (err error) {
 	}
 
 	name := fmt.Sprintf("pgdb-%s", cluster.Id)
-	connectUrl := fmt.Sprintf("postgres://%s:%s@localhost:%s/%s?sslmode=require", name, password, localProxyPort, name)
+	connectUrl := fmt.Sprintf("postgres://%s:%s@localhost:%s/%s", name, password, localProxyPort, name)
 	cmd := exec.CommandContext(ctx, psqlPath, connectUrl)
 	cmd.Stdout = io.Out
 	cmd.Stderr = io.ErrOut
diff --git a/internal/command/root/root.go b/internal/command/root/root.go
index 113725b9d..229767702 100644
--- a/internal/command/root/root.go
+++ b/internal/command/root/root.go
@@ -42,6 +42,7 @@ import (
 	"github.com/superfly/flyctl/internal/command/lfsc"
 	"github.com/superfly/flyctl/internal/command/logs"
 	"github.com/superfly/flyctl/internal/command/machine"
+	"github.com/superfly/flyctl/internal/command/mcp"
 	"github.com/superfly/flyctl/internal/command/metrics"
 	"github.com/superfly/flyctl/internal/command/move"
 	"github.com/superfly/flyctl/internal/command/mpg"
@@ -127,6 +128,7 @@ func New() *cobra.Command {
 		group(ping.New(), "upkeep"),
 		group(proxy.New(), "upkeep"),
 		group(postgres.New(), "dbs_and_extensions"),
+		group(mcp.New(), "upkeep"),
 		group(mpg.New(), "dbs_and_extensions"),
 		group(ips.New(), "configuring"),
 		group(secrets.New(), "configuring"),


Test patch:
diff --git a/internal/command/deploy/plan_test.go b/internal/command/deploy/plan_test.go
index 563955731..2225360d8 100644
--- a/internal/command/deploy/plan_test.go
+++ b/internal/command/deploy/plan_test.go
@@ -227,6 +227,9 @@ func TestUpdateMachines(t *testing.T) {
 			}, nil
 		},
 		ReleaseLeaseFunc: func(ctx context.Context, machineID, nonce string) error {
+			if _, loaded := acquiredLeases.LoadAndDelete(machineID); !loaded {
+				t.Error("Release lease not found for machine:", machineID)
+			}
 			return nil
 		},
 		UpdateFunc: func(ctx context.Context, builder fly.LaunchMachineInput, nonce string) (out *fly.Machine, err error) {
@@ -288,10 +291,11 @@ func TestUpdateMachines(t *testing.T) {
 		app: &fly.AppCompact{
 			Name: "myapp",
 		},
-		appConfig:      &appconfig.Config{AppName: "myapp"},
-		waitTimeout:    10 * time.Second,
-		deployRetries:  5,
-		maxUnavailable: 3,
+		appConfig:       &appconfig.Config{AppName: "myapp"},
+		waitTimeout:     10 * time.Second,
+		deployRetries:   5,
+		maxUnavailable:  3,
+		skipSmokeChecks: true,
 	}
 
 	oldAppState := &AppState{
@@ -303,7 +307,7 @@ func TestUpdateMachines(t *testing.T) {
 	settings := updateMachineSettings{
 		pushForward:          true,
 		skipHealthChecks:     false,
-		skipSmokeChecks:      false,
+		skipSmokeChecks:      true,
 		skipLeaseAcquisition: false,
 	}