Skip to content

Commit d1d2a99

Browse files
authored
Extract MCP server logic into dedicated package (#1403)
Signed-off-by: Juan Antonio Osorio <[email protected]>
1 parent 68ba4bc commit d1d2a99

16 files changed

+1682
-405
lines changed

cmd/thv/app/mcp_serve.go

Lines changed: 12 additions & 381 deletions
Large diffs are not rendered by default.

cmd/thv/app/server.go

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,28 @@
11
package app
22

33
import (
4+
"context"
45
"fmt"
56
"os"
67
"os/signal"
8+
"time"
79

810
"github.com/spf13/cobra"
911

1012
s "github.com/stacklok/toolhive/pkg/api"
1113
"github.com/stacklok/toolhive/pkg/auth"
14+
"github.com/stacklok/toolhive/pkg/logger"
15+
mcpserver "github.com/stacklok/toolhive/pkg/mcp/server"
1216
)
1317

1418
var (
15-
host string
16-
port int
17-
enableDocs bool
18-
socketPath string
19+
host string
20+
port int
21+
enableDocs bool
22+
socketPath string
23+
enableMCPServer bool
24+
mcpServerPort string
25+
mcpServerHost string
1926
)
2027

2128
var serveCmd = &cobra.Command{
@@ -59,6 +66,39 @@ var serveCmd = &cobra.Command{
5966
}
6067
}
6168

69+
// Optionally start MCP server if experimental flag is enabled
70+
if enableMCPServer {
71+
logger.Info("EXPERIMENTAL: Starting embedded MCP server")
72+
73+
// Create MCP server configuration
74+
mcpConfig := &mcpserver.Config{
75+
Host: mcpServerHost,
76+
Port: mcpServerPort,
77+
}
78+
79+
// Create and start the MCP server in a goroutine
80+
mcpServer, err := mcpserver.New(ctx, mcpConfig)
81+
if err != nil {
82+
return fmt.Errorf("failed to create MCP server: %w", err)
83+
}
84+
85+
go func() {
86+
if err := mcpServer.Start(); err != nil {
87+
logger.Errorf("MCP server error: %v", err)
88+
}
89+
}()
90+
91+
// Ensure MCP server is shut down on context cancellation
92+
go func() {
93+
<-ctx.Done()
94+
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
95+
defer shutdownCancel()
96+
if err := mcpServer.Shutdown(shutdownCtx); err != nil {
97+
logger.Errorf("Failed to shutdown MCP server: %v", err)
98+
}
99+
}()
100+
}
101+
62102
return s.Serve(ctx, address, isUnixSocket, debugMode, enableDocs, oidcConfig)
63103
},
64104
}
@@ -71,6 +111,14 @@ func init() {
71111
serveCmd.Flags().StringVar(&socketPath, "socket", "", "UNIX socket path to bind the "+
72112
"server to (overrides host and port if provided)")
73113

114+
// Add experimental MCP server flags
115+
serveCmd.Flags().BoolVar(&enableMCPServer, "experimental-mcp", false,
116+
"EXPERIMENTAL: Enable embedded MCP server for controlling ToolHive")
117+
serveCmd.Flags().StringVar(&mcpServerPort, "experimental-mcp-port", mcpserver.DefaultMCPPort,
118+
"EXPERIMENTAL: Port for the embedded MCP server")
119+
serveCmd.Flags().StringVar(&mcpServerHost, "experimental-mcp-host", "localhost",
120+
"EXPERIMENTAL: Host for the embedded MCP server")
121+
74122
// Add OIDC validation flags
75123
AddOIDCFlags(serveCmd)
76124
}

docs/cli/thv_serve.md

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/mcp/server/get_server_logs.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package server
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/mark3labs/mcp-go/mcp"
9+
)
10+
11+
// getServerLogsArgs holds the arguments for getting server logs
12+
type getServerLogsArgs struct {
13+
Name string `json:"name"`
14+
}
15+
16+
// GetServerLogs gets logs from a running MCP server
17+
func (h *Handler) GetServerLogs(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
18+
// Parse arguments using BindArguments
19+
args := &getServerLogsArgs{}
20+
if err := request.BindArguments(args); err != nil {
21+
return mcp.NewToolResultError(fmt.Sprintf("Failed to parse arguments: %v", err)), nil
22+
}
23+
24+
// Get logs
25+
logs, err := h.workloadManager.GetLogs(ctx, args.Name, false)
26+
if err != nil {
27+
// Check if it's a not found error
28+
if strings.Contains(err.Error(), "not found") {
29+
return mcp.NewToolResultError(fmt.Sprintf("Server '%s' not found", args.Name)), nil
30+
}
31+
return mcp.NewToolResultError(fmt.Sprintf("Failed to get server logs: %v", err)), nil
32+
}
33+
34+
return mcp.NewToolResultText(logs), nil
35+
}

pkg/mcp/server/handler.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Package server provides the MCP (Model Context Protocol) server implementation for ToolHive.
2+
package server
3+
4+
import (
5+
"context"
6+
"fmt"
7+
8+
"github.com/stacklok/toolhive/pkg/registry"
9+
"github.com/stacklok/toolhive/pkg/workloads"
10+
)
11+
12+
// Handler handles MCP tool requests for ToolHive
13+
type Handler struct {
14+
ctx context.Context
15+
workloadManager workloads.Manager
16+
registryProvider registry.Provider
17+
}
18+
19+
// NewHandler creates a new ToolHive handler
20+
func NewHandler(ctx context.Context) (*Handler, error) {
21+
// Create workload manager
22+
workloadManager, err := workloads.NewManager(ctx)
23+
if err != nil {
24+
return nil, fmt.Errorf("failed to create workload manager: %w", err)
25+
}
26+
27+
// Create registry provider
28+
registryProvider, err := registry.GetDefaultProvider()
29+
if err != nil {
30+
return nil, fmt.Errorf("failed to get registry provider: %w", err)
31+
}
32+
33+
return &Handler{
34+
ctx: ctx,
35+
workloadManager: workloadManager,
36+
registryProvider: registryProvider,
37+
}, nil
38+
}

0 commit comments

Comments
 (0)