Skip to content

Commit a6a3195

Browse files
authored
Merge pull request #1048 from dgageot/cagent-mcp-http
Support exposing as a remote MCP server
2 parents c470abd + f0376fa commit a6a3195

2 files changed

Lines changed: 80 additions & 15 deletions

File tree

cmd/root/mcp.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
package root
22

33
import (
4+
"fmt"
5+
46
"github.com/spf13/cobra"
57

68
"github.com/docker/cagent/pkg/config"
79
"github.com/docker/cagent/pkg/mcp"
10+
"github.com/docker/cagent/pkg/server"
811
"github.com/docker/cagent/pkg/telemetry"
912
)
1013

1114
type mcpFlags struct {
15+
http bool
16+
port int
1217
runConfig config.RuntimeConfig
1318
}
1419

@@ -18,15 +23,18 @@ func newMCPCmd() *cobra.Command {
1823
cmd := &cobra.Command{
1924
Use: "mcp <agent-file>|<registry-ref>",
2025
Short: "Start an agent as an MCP (Model Context Protocol) server",
21-
Long: "Start an stdio MCP server that exposes the agent via the Model Context Protocol",
26+
Long: "Start an MCP server that exposes the agent via the Model Context Protocol. By default, uses stdio transport. Use --http to start a streaming HTTP server instead.",
2227
Example: ` cagent mcp ./agent.yaml
2328
cagent mcp ./team.yaml
24-
cagent mcp agentcatalog/pirate`,
29+
cagent mcp agentcatalog/pirate
30+
cagent mcp ./agent.yaml --http --port 8080`,
2531
Args: cobra.ExactArgs(1),
2632
GroupID: "server",
2733
RunE: flags.runMCPCommand,
2834
}
2935

36+
cmd.PersistentFlags().BoolVar(&flags.http, "http", false, "Use streaming HTTP transport instead of stdio")
37+
cmd.PersistentFlags().IntVar(&flags.port, "port", 0, "Port to listen on when using HTTP transport (default: random available port)")
3038
addRuntimeConfigFlags(cmd, &flags.runConfig)
3139

3240
return cmd
@@ -38,5 +46,14 @@ func (f *mcpFlags) runMCPCommand(cmd *cobra.Command, args []string) error {
3846
ctx := cmd.Context()
3947
agentFilename := args[0]
4048

49+
if f.http {
50+
ln, err := server.Listen(ctx, fmt.Sprintf(":%d", f.port))
51+
if err != nil {
52+
return fmt.Errorf("failed to bind to port %d: %w", f.port, err)
53+
}
54+
55+
return mcp.StartHTTPServer(ctx, agentFilename, &f.runConfig, ln)
56+
}
57+
4158
return mcp.StartMCPServer(ctx, agentFilename, &f.runConfig)
4259
}

pkg/mcp/server.go

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"context"
55
"fmt"
66
"log/slog"
7+
"net"
8+
"net/http"
79

810
"github.com/modelcontextprotocol/go-sdk/mcp"
911

@@ -28,21 +30,71 @@ type ToolOutput struct {
2830
func StartMCPServer(ctx context.Context, agentFilename string, runConfig *config.RuntimeConfig) error {
2931
slog.Debug("Starting MCP server", "agent", agentFilename)
3032

31-
agentSource, err := config.Resolve(agentFilename)
33+
server, cleanup, err := createMCPServer(ctx, agentFilename, runConfig)
3234
if err != nil {
3335
return err
3436
}
37+
defer cleanup()
38+
39+
slog.Debug("MCP server starting with stdio transport")
40+
41+
if err := server.Run(ctx, &mcp.StdioTransport{}); err != nil {
42+
return fmt.Errorf("MCP server error: %w", err)
43+
}
44+
45+
return nil
46+
}
47+
48+
// StartHTTPServer starts a streaming HTTP MCP server on the given listener
49+
func StartHTTPServer(ctx context.Context, agentFilename string, runConfig *config.RuntimeConfig, ln net.Listener) error {
50+
slog.Debug("Starting HTTP MCP server", "agent", agentFilename, "addr", ln.Addr())
51+
52+
server, cleanup, err := createMCPServer(ctx, agentFilename, runConfig)
53+
if err != nil {
54+
return err
55+
}
56+
defer cleanup()
57+
58+
fmt.Printf("MCP HTTP server listening on http://%s\n", ln.Addr())
59+
60+
httpServer := &http.Server{
61+
Handler: mcp.NewStreamableHTTPHandler(func(_ *http.Request) *mcp.Server {
62+
return server
63+
}, nil),
64+
}
65+
66+
errCh := make(chan error, 1)
67+
go func() {
68+
errCh <- httpServer.Serve(ln)
69+
}()
70+
71+
select {
72+
case <-ctx.Done():
73+
return httpServer.Shutdown(context.Background())
74+
case err := <-errCh:
75+
if err == http.ErrServerClosed {
76+
return nil
77+
}
78+
return err
79+
}
80+
}
81+
82+
func createMCPServer(ctx context.Context, agentFilename string, runConfig *config.RuntimeConfig) (*mcp.Server, func(), error) {
83+
agentSource, err := config.Resolve(agentFilename)
84+
if err != nil {
85+
return nil, nil, err
86+
}
3587

3688
t, err := teamloader.Load(ctx, agentSource, runConfig)
3789
if err != nil {
38-
return fmt.Errorf("failed to load agents: %w", err)
90+
return nil, nil, fmt.Errorf("failed to load agents: %w", err)
3991
}
4092

41-
defer func() {
93+
cleanup := func() {
4294
if err := t.StopToolSets(ctx); err != nil {
4395
slog.Error("Failed to stop tool sets", "error", err)
4496
}
45-
}()
97+
}
4698

4799
server := mcp.NewServer(&mcp.Implementation{
48100
Name: "cagent",
@@ -55,7 +107,8 @@ func StartMCPServer(ctx context.Context, agentFilename string, runConfig *config
55107
for _, agentName := range agentNames {
56108
ag, err := t.Agent(agentName)
57109
if err != nil {
58-
return fmt.Errorf("failed to get agent %s: %w", agentName, err)
110+
cleanup()
111+
return nil, nil, fmt.Errorf("failed to get agent %s: %w", agentName, err)
59112
}
60113

61114
description := ag.Description()
@@ -67,7 +120,8 @@ func StartMCPServer(ctx context.Context, agentFilename string, runConfig *config
67120

68121
readOnly, err := isReadOnlyAgent(ctx, ag)
69122
if err != nil {
70-
return fmt.Errorf("failed to determine if agent %s is read-only: %w", agentName, err)
123+
cleanup()
124+
return nil, nil, fmt.Errorf("failed to determine if agent %s is read-only: %w", agentName, err)
71125
}
72126

73127
toolDef := &mcp.Tool{
@@ -83,13 +137,7 @@ func StartMCPServer(ctx context.Context, agentFilename string, runConfig *config
83137
mcp.AddTool(server, toolDef, CreateToolHandler(t, agentName))
84138
}
85139

86-
slog.Debug("MCP server starting with stdio transport")
87-
88-
if err := server.Run(ctx, &mcp.StdioTransport{}); err != nil {
89-
return fmt.Errorf("MCP server error: %w", err)
90-
}
91-
92-
return nil
140+
return server, cleanup, nil
93141
}
94142

95143
func CreateToolHandler(t *team.Team, agentName string) func(context.Context, *mcp.CallToolRequest, ToolInput) (*mcp.CallToolResult, ToolOutput, error) {

0 commit comments

Comments
 (0)