Skip to content

Commit 2348dd8

Browse files
KevyVoKevyVojordanstephensnullfunccommit111
authored
Import MCP Server with mcp commands (#1118)
* Import MCP Server This was imported from the repo [here](https://github.com/DefangLabs/defang-mcp) * Made basic CLI command for mcp Added the command mcp and subcommands, [serve, setup] * Implemented the MCP commands serve and setup - mcp serve will satrt the MCP server - mcp setup will create the MCP configuration file, current only supporting vscode, windsurf, cursor and claude * Made MCP cilents process controls Added a way to restart the MCP client process. * Remove logger Cannot use logger due to it tapping into the stdio and does not match with json-rpc format. * Added log file * write kb to user state dir * Run from npx * update vendor hash * edit mcp folder readme * remove knowledge_base files * avoid changing directory when starting mcp server * remove mcp login tool from readme * avoid trying to automatically restart the mcp client * ensure client is lowercase before matching * inline GetValidClientsString * Minor updates Minor review updates * ensure client is lowercase * tighten up IsValidClient * factor out SetupMCPClient func * extract pkg/mcp/setup.go * Update description Co-authored-by: Eric Liu <[email protected]> * read kb and samples_examples from state dir * remove unused output buffer * remove redundant error logging * remove redundant log message * clean up Co-authored-by: Eric Liu <[email protected]> * Refactor Login and share parent resources * update installation to npx * Kevin Merge * Blocking auth github fix We need to do this because when we start the server the broswer will open and does populate with the correct url but the server will not be able to start. By doing this we are able to open the broswer and not be blocked by the server. * update config to use npx --------- Co-authored-by: KevyVo <[email protected]> Co-authored-by: Jordan Stephens <[email protected]> Co-authored-by: Eric Liu <[email protected]> Co-authored-by: commit111 <[email protected]> Co-authored-by: Eric Liu <[email protected]>
1 parent 9149875 commit 2348dd8

File tree

20 files changed

+1170
-23
lines changed

20 files changed

+1170
-23
lines changed

pkgs/defang/cli.nix

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ buildGoModule {
77
pname = "defang-cli";
88
version = "git";
99
src = ../../src;
10-
vendorHash = "sha256-Q2dwaHlxzbJBMTuIKiLmJNdT0LfoHgmTOHprFTK3BTc=";
10+
vendorHash = "sha256-wy6S3alGpeXveCPR8BnyaGcsIHzgclv3yrofwE5X3oI="; # TODO: use fetchFromGitHub
1111

1212
subPackages = [ "cmd/cli" ];
1313

1414
nativeBuildInputs = [ installShellFiles ];
1515

16-
CGO_ENABLED = 0;
16+
env.CGO_ENABLED = 0;
1717
ldflags = [
1818
"-s"
1919
"-w"

src/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ defang-arm64
99
dist/
1010
distx/
1111
defang.exe
12+
samples_examples.json
13+
knowledge_base.json

src/cmd/cli/command/commands.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,13 @@ func SetupCommands(ctx context.Context, version string) {
269269
deploymentsCmd.AddCommand(deploymentsListCmd)
270270
RootCmd.AddCommand(deploymentsCmd)
271271

272+
// MCP Command
273+
mcpCmd.AddCommand(mcpSetupCmd)
274+
mcpCmd.AddCommand(mcpServerCmd)
275+
mcpSetupCmd.Flags().String("client", "", "MCP setup client (supports: claude, windsurf, cursor, vscode)")
276+
mcpSetupCmd.MarkFlagRequired("client")
277+
RootCmd.AddCommand(mcpCmd)
278+
272279
// Send Command
273280
sendCmd.Flags().StringP("subject", "n", "", "subject to send the message to (required)")
274281
sendCmd.Flags().StringP("type", "t", "", "type of message to send (required)")

src/cmd/cli/command/mcp.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package command
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"strings"
7+
8+
cliClient "github.com/DefangLabs/defang/src/pkg/cli/client"
9+
"github.com/DefangLabs/defang/src/pkg/mcp"
10+
"github.com/DefangLabs/defang/src/pkg/mcp/resources"
11+
"github.com/DefangLabs/defang/src/pkg/mcp/tools"
12+
"github.com/DefangLabs/defang/src/pkg/term"
13+
"github.com/mark3labs/mcp-go/server"
14+
"github.com/spf13/cobra"
15+
)
16+
17+
var mcpCmd = &cobra.Command{
18+
Use: "mcp",
19+
Short: "Manage MCP Server for defang",
20+
Annotations: authNeededAnnotation,
21+
}
22+
23+
var mcpServerCmd = &cobra.Command{
24+
Use: "serve",
25+
Short: "Start defang MCP server",
26+
Annotations: authNeededAnnotation,
27+
Args: cobra.NoArgs,
28+
RunE: func(cmd *cobra.Command, args []string) error {
29+
30+
logFile, err := os.OpenFile(filepath.Join(cliClient.StateDir, "defang-mcp.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
31+
if err != nil {
32+
term.Error("Failed to open log file", "error", err)
33+
return err
34+
}
35+
defer logFile.Close()
36+
37+
// TODO: Should we still write to a file or we can go back to stderr
38+
term.DefaultTerm = term.NewTerm(os.Stdin, logFile, logFile)
39+
40+
// Setup knowledge base
41+
if err := mcp.SetupKnowledgeBase(); err != nil {
42+
term.Error("Failed to setup knowledge base", "error", err)
43+
return err
44+
}
45+
46+
term.Info("Starting Defang MCP server")
47+
48+
// Create a new MCP server
49+
term.Info("Creating MCP server")
50+
s := server.NewMCPServer(
51+
"Defang Services",
52+
"1.0.0",
53+
server.WithResourceCapabilities(true, true), // Enable resource management and notifications
54+
server.WithPromptCapabilities(true), // Enable interactive prompts
55+
server.WithToolCapabilities(true), // Enable dynamic tool list updates
56+
server.WithInstructions("You are an MCP server for Defang Services. Your role is to manage and deploy services efficiently using the provided tools and resources."),
57+
)
58+
59+
// Setup resources
60+
resources.SetupResources(s)
61+
62+
// Setup tools
63+
tools.SetupTools(s, client, getCluster(), gitHubClientId)
64+
65+
// Start the server
66+
term.Info("Starting Defang Services MCP server")
67+
if err := server.ServeStdio(s); err != nil {
68+
return err
69+
}
70+
71+
term.Info("Server shutdown")
72+
73+
return nil
74+
},
75+
}
76+
77+
var mcpSetupCmd = &cobra.Command{
78+
Use: "setup",
79+
Short: "Setup MCP client for defang mcp server",
80+
Annotations: authNeededAnnotation,
81+
Args: cobra.NoArgs,
82+
RunE: func(cmd *cobra.Command, args []string) error {
83+
client, _ := cmd.Flags().GetString("client")
84+
client = strings.ToLower(client)
85+
if err := mcp.SetupClient(client); err != nil {
86+
return err
87+
}
88+
89+
return nil
90+
},
91+
}

src/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ require (
3636
github.com/gorilla/websocket v1.5.0
3737
github.com/hashicorp/go-retryablehttp v0.7.7
3838
github.com/hexops/gotextdiff v1.0.3
39+
github.com/mark3labs/mcp-go v0.21.0
3940
github.com/miekg/dns v1.1.59
4041
github.com/moby/patternmatcher v0.6.0
4142
github.com/muesli/termenv v0.15.2
@@ -93,6 +94,7 @@ require (
9394
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
9495
github.com/rivo/uniseg v0.4.2 // indirect
9596
github.com/russross/blackfriday/v2 v2.1.0 // indirect
97+
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
9698
go.opencensus.io v0.24.0 // indirect
9799
go.opentelemetry.io/contrib/detectors/gcp v1.29.0 // indirect
98100
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect

src/go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
239239
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
240240
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
241241
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
242+
github.com/mark3labs/mcp-go v0.21.0 h1:oyEtiXg8PnrVEFis9b1AwbiUWF2dTbyBP5yLo7SruXE=
243+
github.com/mark3labs/mcp-go v0.21.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE=
242244
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
243245
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
244246
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
@@ -314,6 +316,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo
314316
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
315317
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
316318
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
319+
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
320+
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
317321
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
318322
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
319323
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=

src/pkg/cli/login.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func (g GitHubAuthService) login(
4747
) (string, error) {
4848
term.Debug("Logging in to", fabric)
4949

50-
code, err := github.StartAuthCodeFlow(ctx, gitHubClientId)
50+
code, err := github.StartAuthCodeFlow(ctx, gitHubClientId, true)
5151
if err != nil {
5252
return "", err
5353
}

src/pkg/cli/token.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func Token(ctx context.Context, client client.FabricClient, clientId string, ten
1717
return ErrDryRun
1818
}
1919

20-
code, err := github.StartAuthCodeFlow(ctx, clientId)
20+
code, err := github.StartAuthCodeFlow(ctx, clientId, false)
2121
if err != nil {
2222
return err
2323
}

src/pkg/github/auth.go

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,9 @@ var (
5656
authTemplate = template.Must(template.New("auth").Parse(authTemplateString))
5757
)
5858

59-
func StartAuthCodeFlow(ctx context.Context, clientId string) (string, error) {
59+
func StartAuthCodeFlow(ctx context.Context, clientId string, prompt bool) (string, error) {
6060
ctx, cancel := context.WithCancel(ctx)
61+
defer cancel()
6162

6263
// Generate random state
6364
state := uuid.NewString()
@@ -102,25 +103,29 @@ func StartAuthCodeFlow(ctx context.Context, clientId string) (string, error) {
102103
}
103104
authorizeUrl = "https://github.com/login/oauth/authorize?" + values.Encode()
104105

105-
n, _ := term.Printf("Please visit %s and log in. (Right click the URL or press ENTER to open browser)\r", server.URL)
106-
defer term.Print(strings.Repeat(" ", n), "\r") // TODO: use termenv to clear line
107-
108-
input := term.NewNonBlockingStdin()
109-
defer input.Close() // abort the read
110-
go func() {
111-
var b [1]byte
112-
for {
113-
if _, err := input.Read(b[:]); err != nil {
114-
return // exit goroutine
115-
}
116-
switch b[0] {
117-
case 3: // Ctrl-C
118-
cancel()
119-
case 10, 13: // Enter or Return
120-
browser.OpenURL(server.URL)
106+
// TODO:This is used to open the browser for GitHub Auth before blocking
107+
if !prompt {
108+
browser.OpenURL(server.URL)
109+
} else {
110+
n, _ := term.Printf("Please visit %s and log in. (Right click the URL or press ENTER to open browser)\r", server.URL)
111+
defer term.Print(strings.Repeat(" ", n), "\r") // TODO: use termenv to clear line
112+
input := term.NewNonBlockingStdin()
113+
defer input.Close() // abort the read
114+
go func() {
115+
var b [1]byte
116+
for {
117+
if _, err := input.Read(b[:]); err != nil {
118+
return // exit goroutine
119+
}
120+
switch b[0] {
121+
case 3: // Ctrl-C
122+
cancel()
123+
case 10, 13: // Enter or Return
124+
browser.OpenURL(server.URL)
125+
}
121126
}
122-
}
123-
}()
127+
}()
128+
}
124129

125130
select {
126131
case <-ctx.Done():

src/pkg/mcp/README.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# ![Defang](https://raw.githubusercontent.com/DefangLabs/defang-assets/main/Logos/Element_Wordmark_Slogan/JPG/Dark_Colour_Glow.jpg)
2+
3+
# Defang MCP Server
4+
5+
This folder contains a Model Context Protocol (MCP) server with built-in Defang tools (`deploy`, `services`, `destroy`) to allow users to manage their services with AI coding agents in a supported IDE.
6+
7+
Below is an installation guide to get started.
8+
9+
## Installation
10+
11+
First, make sure you have the [npm package manager](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) installed.
12+
13+
Connect the MCP server with your IDE by running the following command in your terminal:
14+
15+
```bash
16+
npx -y defang mcp setup --client=<your-ide>
17+
```
18+
19+
Replace `<your-ide>` with the name of your preferred IDE. See our list of [Supported IDEs](#supported-ides).
20+
21+
After setup, you can start the MCP server with the command:
22+
23+
```bash
24+
npx -y defang mcp serve
25+
```
26+
27+
Once the server is running, you can access the Defang MCP tools directly through the AI agent chat in your IDE.
28+
29+
## Supported IDEs
30+
31+
### Cursor
32+
33+
```bash
34+
npx -y defang mcp setup --client=cursor
35+
```
36+
37+
### Windsurf
38+
39+
```bash
40+
npx -y defang mcp setup --client=windsurf
41+
```
42+
43+
### VSCode
44+
45+
```bash
46+
npx -y defang mcp setup --client=vscode
47+
```
48+
49+
### Claude Desktop
50+
51+
(While this is not an IDE in the traditional sense, it can support MCP servers.)
52+
53+
```bash
54+
npx -y defang mcp setup --client=claude
55+
```
56+
57+
## MCP Tools
58+
59+
Below are the tools available in the Defang MCP Server.
60+
61+
### `deploy`
62+
63+
The `deploy` tool scans your project directory for Dockerfiles and `compose.yaml` files, then deploys the detected service(s) using Defang. You can monitor the deployment process in the Defang Portal.
64+
65+
### `services`
66+
67+
The `services` tool displays the details of all your services that are currently deployed with Defang. It shows the Service Name, Deployment ID, Public URL and Service Status. If there are no services found, it will display an appropriate message.
68+
69+
### `destroy`
70+
71+
Given a project name or directory, the `destroy` tool identifies any services deployed with Defang and terminates them. If no services are found, it will display an appropriate message.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package deployment_info
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/DefangLabs/defang/src/pkg/cli/client"
8+
"github.com/DefangLabs/defang/src/pkg/term"
9+
defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1"
10+
)
11+
12+
type ErrNoServices struct {
13+
ProjectName string // may be empty
14+
}
15+
16+
func (e ErrNoServices) Error() string {
17+
return fmt.Sprintf("no services found in project %q", e.ProjectName)
18+
}
19+
20+
type Service struct {
21+
Service string
22+
DeploymentId string
23+
PublicFqdn string
24+
PrivateFqdn string
25+
Status string
26+
}
27+
28+
func GetServices(ctx context.Context, projectName string, provider client.Provider) ([]Service, error) {
29+
term.Infof("Listing services in project %q", projectName)
30+
31+
getServicesResponse, err := provider.GetServices(ctx, &defangv1.GetServicesRequest{Project: projectName})
32+
if err != nil {
33+
return nil, err
34+
}
35+
36+
numServices := len(getServicesResponse.Services)
37+
38+
if numServices == 0 {
39+
return nil, ErrNoServices{ProjectName: projectName}
40+
}
41+
42+
result := make([]Service, numServices)
43+
for i, si := range getServicesResponse.Services {
44+
result[i] = Service{
45+
Service: si.Service.Name,
46+
DeploymentId: si.Etag,
47+
PublicFqdn: si.PublicFqdn,
48+
PrivateFqdn: si.PrivateFqdn,
49+
Status: si.Status,
50+
}
51+
}
52+
53+
return result, nil
54+
}

0 commit comments

Comments
 (0)