-
Notifications
You must be signed in to change notification settings - Fork 449
Expand file tree
/
Copy pathmcp_inspect_mcp_scripts_server.go
More file actions
214 lines (187 loc) · 7.16 KB
/
Copy pathmcp_inspect_mcp_scripts_server.go
File metadata and controls
214 lines (187 loc) · 7.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
package cli
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"time"
"github.com/github/gh-aw/pkg/console"
"github.com/github/gh-aw/pkg/parser"
"github.com/github/gh-aw/pkg/types"
"github.com/github/gh-aw/pkg/workflow"
)
const (
// Port range for mcp-scripts HTTP server
mcpScriptsStartPort = 3000
mcpScriptsPortRange = 10
// mcpScriptsServerStartupDelay paces readiness checks while the mcp-scripts server boots.
mcpScriptsServerStartupDelay = 200 * time.Millisecond
)
// findAvailablePort finds an available port starting from the given port
func findAvailablePort(startPort int, verbose bool) int {
for port := startPort; port < startPort+mcpScriptsPortRange; port++ {
listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
if err == nil {
// Close listener and check for errors
if err := listener.Close(); err != nil && verbose {
mcpInspectLog.Printf("Warning: Failed to close listener on port %d: %v", port, err)
}
if verbose {
mcpInspectLog.Printf("Found available port: %d", port)
}
return port
}
}
return 0
}
// waitForServerReady waits for the HTTP server to be ready by polling the endpoint
func waitForServerReady(ctx context.Context, port int, timeout time.Duration, verbose bool) bool {
deadline := time.Now().Add(timeout)
client := &http.Client{
Timeout: 1 * time.Second,
}
url := fmt.Sprintf("http://localhost:%d/", port)
for time.Now().Before(deadline) {
select {
case <-ctx.Done():
return false
default:
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
mcpInspectLog.Printf("Failed to create request: %v", err)
return false
}
resp, err := client.Do(req)
if err == nil {
if closeErr := resp.Body.Close(); closeErr != nil {
mcpInspectLog.Printf("Warning: failed to close response body: %v", closeErr)
}
if verbose {
mcpInspectLog.Printf("Server is ready on port %d", port)
}
return true
}
timer := time.NewTimer(mcpScriptsServerStartupDelay)
select {
case <-ctx.Done():
timer.Stop()
return false
case <-timer.C:
}
}
mcpInspectLog.Printf("Server did not become ready within timeout")
return false
}
// startMCPScriptsHTTPServer starts the mcp-scripts HTTP MCP server
func startMCPScriptsHTTPServer(dir string, port int, verbose bool) (*exec.Cmd, error) {
mcpInspectLog.Printf("Starting mcp-scripts HTTP server on port %d", port)
mcpServerPath := filepath.Join(dir, "mcp-server.cjs")
cmd := exec.Command("node", mcpServerPath)
cmd.Dir = dir
cmd.Env = append(os.Environ(),
fmt.Sprintf("GH_AW_MCP_SCRIPTS_PORT=%d", port),
)
// Capture output for debugging
if verbose {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
if err := cmd.Start(); err != nil {
errMsg := fmt.Sprintf("failed to start server: %v", err)
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(errMsg))
return nil, fmt.Errorf("failed to start server: %w", err)
}
if verbose {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Started mcp-scripts server (PID: %d)", cmd.Process.Pid)))
}
return cmd, nil
}
// startMCPScriptsServer starts the mcp-scripts HTTP server and returns the MCP config
func startMCPScriptsServer(ctx context.Context, mcpScriptsConfig *workflow.MCPScriptsConfig, verbose bool) (*parser.RegistryMCPServerConfig, *exec.Cmd, string, error) {
mcpInspectLog.Printf("Starting mcp-scripts server with %d tools", len(mcpScriptsConfig.Tools))
// Check if node is available
if _, err := exec.LookPath("node"); err != nil {
errMsg := "node not found. Please install Node.js to run the mcp-scripts MCP server"
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(errMsg))
return nil, nil, "", fmt.Errorf("node not found. Please install Node.js to run the mcp-scripts MCP server: %w", err)
}
if verbose {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Found %d mcp-script tool(s) to configure", len(mcpScriptsConfig.Tools))))
}
// Create temporary directory for mcp-scripts files
tmpDir, err := os.MkdirTemp("", "gh-aw-mcp-scripts-*")
if err != nil {
errMsg := fmt.Sprintf("failed to create temporary directory: %v", err)
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(errMsg))
return nil, nil, "", fmt.Errorf("failed to create temporary directory: %w", err)
}
if verbose {
if _, err := fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Created temporary directory: "+tmpDir)); err != nil {
mcpInspectLog.Printf("Warning: failed to write to stderr: %v", err)
}
}
// Write mcp-scripts files to temporary directory
if err := writeMCPScriptsFiles(tmpDir, mcpScriptsConfig, verbose); err != nil {
// Clean up temporary directory on error
if err := os.RemoveAll(tmpDir); err != nil && verbose {
mcpInspectLog.Printf("Warning: failed to clean up temporary directory %s: %v", tmpDir, err)
}
errMsg := fmt.Sprintf("failed to write mcp-scripts files: %v", err)
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(errMsg))
return nil, nil, "", fmt.Errorf("failed to write mcp-scripts files: %w", err)
}
// Find an available port for the HTTP server
port := findAvailablePort(mcpScriptsStartPort, verbose)
if port == 0 {
if err := os.RemoveAll(tmpDir); err != nil && verbose {
mcpInspectLog.Printf("Warning: failed to clean up temporary directory %s: %v", tmpDir, err)
}
errMsg := "failed to find an available port for the HTTP server"
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(errMsg))
return nil, nil, "", errors.New("failed to find an available port for the HTTP server")
}
if verbose {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Using port %d for mcp-scripts HTTP server", port)))
}
// Start the HTTP server
serverCmd, err := startMCPScriptsHTTPServer(tmpDir, port, verbose)
if err != nil {
// Clean up temporary directory on error
if rmErr := os.RemoveAll(tmpDir); rmErr != nil && verbose {
mcpInspectLog.Printf("Warning: failed to clean up temporary directory %s: %v", tmpDir, rmErr)
}
return nil, nil, "", fmt.Errorf("failed to start mcp-scripts HTTP server: %w", err)
}
// Wait for the server to start up
if !waitForServerReady(ctx, port, 5*time.Second, verbose) {
if serverCmd.Process != nil {
// Kill the process and log warning if it fails
if err := serverCmd.Process.Kill(); err != nil && verbose {
mcpInspectLog.Printf("Warning: failed to kill server process %d: %v", serverCmd.Process.Pid, err)
}
}
if err := os.RemoveAll(tmpDir); err != nil && verbose {
mcpInspectLog.Printf("Warning: failed to clean up temporary directory %s: %v", tmpDir, err)
}
return nil, nil, "", errors.New("mcp-scripts HTTP server failed to start within timeout")
}
if verbose {
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("MCP Scripts HTTP server started successfully"))
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Server running on: http://localhost:%d", port)))
}
// Create MCP server config for the mcp-scripts server
config := &parser.RegistryMCPServerConfig{
BaseMCPServerConfig: types.BaseMCPServerConfig{
Type: "http",
URL: fmt.Sprintf("http://localhost:%d", port),
Env: make(map[string]string),
},
Name: "mcpscripts",
}
return config, serverCmd, tmpDir, nil
}