#
tokens: 10537/50000 3/3 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── config.go
├── go.mod
├── go.sum
├── main.go
└── README.md
```

# Files

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | 
 2 | # GPT MCP Proxy
 3 | 
 4 | A REST API server that provides HTTP access to Multiple Command Protocol (MCP) tools. This server acts as a bridge between HTTP clients and MCP-compliant tool servers, allowing tools to be discovered and executed via REST endpoints.
 5 | This is very useful for integrating MCP tools with custom GPT through Actions.
 6 | 
 7 | ## Features
 8 | 
 9 | - List available MCP servers and their tools
10 | - Get detailed information about specific tools
11 | - Execute tools with custom parameters
12 | - OpenAPI 3.1.0 specification
13 | - Automatic public HTTPS exposure via ngrok
14 | 
15 | ## Prerequisites
16 | 
17 | - Go 1.20 or later
18 | - ngrok account and authtoken
19 | - MCP-compliant tools
20 | 
21 | ## Configuration
22 | 
23 | The server requires the following environment variables:
24 | 
25 | ```bash
26 | NGROK_AUTH_TOKEN=your_ngrok_auth_token
27 | NGROK_DOMAIN=your_ngrok_domain
28 | MCP_CONFIG_FILE=/path/to/mcp_settings.json  # Optional, defaults to mcp_settings.json
29 | ```
30 | 
31 | ### Configuration File Format
32 | 
33 | Create an `mcp_settings.json` file with your MCP server configurations:
34 | 
35 | ```json
36 | {
37 |   "mcpServers": {
38 |     "filesystem": {
39 |       "command": "npx",
40 |       "args": [
41 |         "-y",
42 |         "@modelcontextprotocol/server-filesystem",
43 |         "/Users/username/Desktop",
44 |         "/path/to/other/allowed/dir"
45 |       ]
46 |     }
47 |   }
48 | }
49 | ```
50 | 
51 | ## API Endpoints
52 | 
53 | - `GET /openapi.json` - OpenAPI specification
54 | - `GET /mcp/servers` - List all servers and their tools
55 | - `GET /mcp/{serverName}` - Get server details
56 | - `GET /mcp/{serverName}/tools/{toolName}` - Get tool details
57 | - `POST /mcp/{serverName}/tools/{toolName}/execute` - Execute a tool
58 | 
59 | ## Usage
60 | 
61 | 1. Set up environment variables
62 | 2. Prepare configuration file
63 | 3. Run the server:
64 | 
65 | ```bash
66 | go run main.go
67 | ```
68 | 
69 | ## Development
70 | 
71 | To build from source:
72 | 
73 | ```bash
74 | git clone https://github.com/wricardo/mcp-http-server.git
75 | cd mcp-http-server
76 | go build
77 | ```
78 | 
79 | ## License
80 | 
81 | MIT License
82 | 
83 | ## Contributing
84 | 
85 | Contributions are welcome! Please feel free to submit a Pull Request.
86 | 
```

--------------------------------------------------------------------------------
/config.go:
--------------------------------------------------------------------------------

```go
 1 | package main
 2 | 
 3 | import (
 4 | 	"encoding/json"
 5 | 	"io/ioutil"
 6 | 	"os"
 7 | )
 8 | 
 9 | // MCPServerConfig represents the configuration for an MCP server.
10 | type MCPServerConfig struct {
11 | 	Command     string            `json:"command"`
12 | 	Args        []string          `json:"args"`
13 | 	Env         map[string]string `json:"env"`
14 | 	Disabled    bool              `json:"disabled"`
15 | 	AutoApprove []string          `json:"autoApprove"`
16 | 	Timeout     int               `json:"timeout,omitempty"`
17 | }
18 | 
19 | // AppConfig represents the overall configuration for the application.
20 | type AppConfig struct {
21 | 	MCPServers map[string]MCPServerConfig `json:"mcpServers"`
22 | }
23 | 
24 | // LoadConfig reads the configuration from a file and returns an AppConfig struct.
25 | func LoadConfig(filename string) (*AppConfig, error) {
26 | 	file, err := os.Open(filename)
27 | 	if err != nil {
28 | 		return nil, err
29 | 	}
30 | 	defer file.Close()
31 | 
32 | 	bytes, err := ioutil.ReadAll(file)
33 | 	if err != nil {
34 | 		return nil, err
35 | 	}
36 | 
37 | 	var config AppConfig
38 | 	if err := json.Unmarshal(bytes, &config); err != nil {
39 | 		return nil, err
40 | 	}
41 | 
42 | 	return &config, nil
43 | }
44 | 
```

--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------

```go
  1 | // Package main provides an HTTP server for MCP (sulti-Tool Coordination Protocol) tools
  2 | // It exposes REST endpoints that allow listing, describing, and executing MCP tools
  3 | package main
  4 | 
  5 | import (
  6 | 	"bytes"
  7 | 	"context"
  8 | 	"encoding/json"
  9 | 	"fmt"
 10 | 	"log"
 11 | 	"net/http"
 12 | 	"os"
 13 | 	"strings"
 14 | 	"time"
 15 | 
 16 | 	"github.com/davecgh/go-spew/spew"
 17 | 	"github.com/go-openapi/spec"
 18 | 	"github.com/gorilla/mux"
 19 | 	mcpclient "github.com/mark3labs/mcp-go/client"
 20 | 	"github.com/mark3labs/mcp-go/mcp"
 21 | 	"golang.ngrok.com/ngrok"
 22 | 	"golang.ngrok.com/ngrok/config"
 23 | 	"golang.org/x/net/http2"
 24 | 	"golang.org/x/net/http2/h2c"
 25 | )
 26 | 
 27 | func main() {
 28 | 	// Check for required environment variables
 29 | 	ngrokToken := os.Getenv("NGROK_AUTH_TOKEN")
 30 | 	ngrokDomain := os.Getenv("NGROK_DOMAIN")
 31 | 	if ngrokToken == "" || ngrokDomain == "" {
 32 | 		log.Fatal("NGROK_AUTH_TOKEN and NGROK_DOMAIN environment variables must be set")
 33 | 	}
 34 | 	fmt.Println("Using ngrok domain:", ngrokDomain)
 35 | 	fmt.Println("Using ngrok auth token:", ngrokToken)
 36 | 
 37 | 	// Get config file path from environment or use default
 38 | 	configFile := os.Getenv("MCP_CONFIG_FILE")
 39 | 	if configFile == "" {
 40 | 		log.Println("MCP_CONFIG_FILE not set, using default path")
 41 | 		configFile = "mcp_settings.json"
 42 | 	}
 43 | 
 44 | 	// Load configuration from file
 45 | 	cfg, err := LoadConfig(configFile)
 46 | 	if err != nil {
 47 | 		log.Fatalf("Error loading config: %v", err)
 48 | 	}
 49 | 
 50 | 	// Initialize MCP servers registry and clients
 51 | 	mcpServers = make(map[string]MCPServerInfo)
 52 | 	clients = make(map[string]mcpclient.MCPClient)
 53 | 
 54 | 	// Register MCP servers from config
 55 | 	for name, serverConfig := range cfg.MCPServers {
 56 | 		if serverConfig.Disabled {
 57 | 			continue
 58 | 		}
 59 | 		mcpServers[name] = MCPServerInfo{
 60 | 			Name:    name,
 61 | 			Command: serverConfig.Command,
 62 | 			Env:     convertMapToSlice(serverConfig.Env),
 63 | 			Args:    serverConfig.Args,
 64 | 		}
 65 | 	}
 66 | 
 67 | 	// Initialize MCP clients for each server
 68 | 	for name, info := range mcpServers {
 69 | 		client, err := mcpclient.NewStdioMCPClient(info.Command, info.Env, info.Args...)
 70 | 		if err != nil {
 71 | 			log.Fatalf("Error initializing MCP client for %s: %v", name, err)
 72 | 		}
 73 | 		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 74 | 		defer cancel()
 75 | 
 76 | 		_, err = client.Initialize(ctx, mcp.InitializeRequest{})
 77 | 		if err != nil {
 78 | 			log.Fatalf("Error initializing MCP client for %s: %v", name, err)
 79 | 		}
 80 | 		log.Printf("Successfully initialized MCP client for %s", name)
 81 | 		clients[name] = client
 82 | 	}
 83 | 
 84 | 	// Set up HTTP routes using Gorilla Mux
 85 | 	router := mux.NewRouter()
 86 | 	router.HandleFunc("/", handleIndex).Methods("GET")
 87 | 	router.HandleFunc("/openapi.json", handleOpenAPISpec).Methods("GET")
 88 | 	router.HandleFunc("/instructions.txt", handleInstructions).Methods("GET")
 89 | 	router.HandleFunc("/servers", listServersToolsHandler(mcpServers)).Methods("GET")
 90 | 	router.HandleFunc("/{serverName}", describeServerHandler).Methods("GET")
 91 | 	router.HandleFunc("/{serverName}/{toolName}", getToolDetailsHandler).Methods("GET")
 92 | 	router.HandleFunc("/{serverName}/{toolName}", executeToolHandler).Methods("POST")
 93 | 
 94 | 	// Start the server using ngrok for public exposure
 95 | 	ctx := context.Background()
 96 | 	listener, err := ngrok.Listen(ctx,
 97 | 		config.HTTPEndpoint(config.WithDomain(ngrokDomain)),
 98 | 		ngrok.WithAuthtokenFromEnv(),
 99 | 	)
100 | 	if err != nil {
101 | 		log.Fatal(err)
102 | 	}
103 | 
104 | 	log.Printf("MCP HTTP server is running at https://%s", ngrokDomain)
105 | 	err = http.Serve(listener, h2c.NewHandler(router, &http2.Server{}))
106 | 	if err != nil {
107 | 		log.Fatalf("Server error: %v", err)
108 | 	}
109 | }
110 | 
111 | // MCPServerInfo represents a registered MCP server configuration
112 | type MCPServerInfo struct {
113 | 	Name    string   `json:"name"`
114 | 	Command string   `json:"command"`
115 | 	Env     []string `json:"env"`
116 | 	Args    []string `json:"args"`
117 | }
118 | 
119 | // Global registry of MCP servers
120 | var mcpServers map[string]MCPServerInfo
121 | 
122 | // Global map of initialized MCP clients
123 | var clients map[string]mcpclient.MCPClient
124 | 
125 | // ToolExecutionRequest represents the payload for executing a tool
126 | type ToolExecutionRequest struct {
127 | 	InputData string                 `json:"input_data"`
128 | 	Config    map[string]interface{} `json:"config"`
129 | }
130 | 
131 | // describeServerHandler queries the MCP server for the details of the server and its tools
132 | func describeServerHandler(w http.ResponseWriter, r *http.Request) {
133 | 	vars := mux.Vars(r)
134 | 	serverName := vars["serverName"]
135 | 
136 | 	client, ok := clients[serverName]
137 | 	if !ok {
138 | 		http.Error(w, fmt.Sprintf("MCP client not found for server: %s", serverName), http.StatusNotFound)
139 | 		return
140 | 	}
141 | 
142 | 	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
143 | 	defer cancel()
144 | 
145 | 	listReq := mcp.ListToolsRequest{}
146 | 	listResp, err := client.ListTools(ctx, listReq)
147 | 	if err != nil {
148 | 		http.Error(w, "Error listing tools: "+err.Error(), http.StatusInternalServerError)
149 | 		return
150 | 	}
151 | 
152 | 	serverInfo, ok := mcpServers[serverName]
153 | 	if !ok {
154 | 		http.Error(w, "Server info not found", http.StatusInternalServerError)
155 | 		return
156 | 	}
157 | 
158 | 	// Define server description structure for response
159 | 	type ServerDescription struct {
160 | 		Server MCPServerInfo `json:"server"`
161 | 		Tools  []mcp.Tool    `json:"tools"`
162 | 	}
163 | 
164 | 	serverDescription := ServerDescription{
165 | 		Server: serverInfo,
166 | 		Tools:  listResp.Tools,
167 | 	}
168 | 
169 | 	w.Header().Set("Content-Type", "application/json")
170 | 	json.NewEncoder(w).Encode(serverDescription)
171 | }
172 | 
173 | // getToolDetailsHandler returns details for a specified tool
174 | func getToolDetailsHandler(w http.ResponseWriter, r *http.Request) {
175 | 	vars := mux.Vars(r)
176 | 	serverName := vars["serverName"]
177 | 	toolName := vars["toolName"]
178 | 
179 | 	client, ok := clients[serverName]
180 | 	if !ok {
181 | 		http.Error(w, fmt.Sprintf("MCP client not found for server: %s", serverName), http.StatusNotFound)
182 | 		return
183 | 	}
184 | 
185 | 	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
186 | 	defer cancel()
187 | 
188 | 	listReq := mcp.ListToolsRequest{}
189 | 	listResp, err := client.ListTools(ctx, listReq)
190 | 	if err != nil {
191 | 		http.Error(w, "Error listing tools: "+err.Error(), http.StatusInternalServerError)
192 | 		return
193 | 	}
194 | 
195 | 	var found *mcp.Tool
196 | 	for _, tool := range listResp.Tools {
197 | 		if tool.Name == toolName {
198 | 			found = &tool
199 | 			break
200 | 		}
201 | 	}
202 | 
203 | 	if found == nil {
204 | 		http.Error(w, fmt.Sprintf("Tool %s not found", toolName), http.StatusNotFound)
205 | 		return
206 | 	}
207 | 
208 | 	w.Header().Set("Content-Type", "application/json")
209 | 	json.NewEncoder(w).Encode(found)
210 | }
211 | 
212 | // executeToolHandler invokes a tool on the MCP server synchronously
213 | func executeToolHandler(w http.ResponseWriter, r *http.Request) {
214 | 	vars := mux.Vars(r)
215 | 	serverName := vars["serverName"]
216 | 	toolName := vars["toolName"]
217 | 
218 | 	client, ok := clients[serverName]
219 | 	if !ok {
220 | 		http.Error(w, fmt.Sprintf("MCP client not found for server: %s", serverName), http.StatusNotFound)
221 | 		return
222 | 	}
223 | 
224 | 	var args map[string]interface{}
225 | 	if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
226 | 		http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest)
227 | 		return
228 | 	}
229 | 
230 | 	// Use a longer timeout for tool execution
231 | 	ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
232 | 	defer cancel()
233 | 
234 | 	// Build the tool call request
235 | 	callReq := mcp.CallToolRequest{
236 | 		Request: mcp.Request{
237 | 			Method: "tools/call",
238 | 		},
239 | 	}
240 | 
241 | 	callReq.Params.Name = toolName
242 | 	callReq.Params.Arguments = args
243 | 
244 | 	log.Printf("Tool name: %s\n", toolName)
245 | 	encoded, err := json.Marshal(args)
246 | 	if err != nil {
247 | 		log.Printf("Error encoding args: %v\n", err)
248 | 	}
249 | 	log.Printf("Args: %s\n", string(encoded))
250 | 
251 | 	result, err := client.CallTool(ctx, callReq)
252 | 	// log the the request and result. Pretty.
253 | 	if err != nil {
254 | 		log.Printf("error: %v\n", err)
255 | 	}
256 | 	log.Printf("error: %v\n", err)
257 | 	if result != nil {
258 | 		if result.IsError {
259 | 			log.Println("response is an error.")
260 | 
261 | 		}
262 | 		log.Println("Response:")
263 | 		for _, content := range result.Content {
264 | 			casted := content.(mcp.TextContent)
265 | 			fmt.Println(casted.Text)
266 | 		}
267 | 	}
268 | 
269 | 	if err != nil {
270 | 		errorMsg := fmt.Sprintf("Error executing tool %s: %v", toolName, err)
271 | 		http.Error(w, errorMsg, http.StatusInternalServerError)
272 | 		return
273 | 	}
274 | 
275 | 	w.Header().Set("Content-Type", "application/json")
276 | 	json.NewEncoder(w).Encode(result)
277 | }
278 | 
279 | // convertMapToSlice converts a map of environment variables to a slice of strings in "key=value" format
280 | func convertMapToSlice(envMap map[string]string) []string {
281 | 	var env []string
282 | 	for k, v := range envMap {
283 | 		env = append(env, k+"="+v)
284 | 	}
285 | 	return env
286 | }
287 | 
288 | // listServersToolsHandler returns a handler function that lists all registered servers and their tools
289 | func listServersToolsHandler(mcpServers map[string]MCPServerInfo) http.HandlerFunc {
290 | 	return func(w http.ResponseWriter, r *http.Request) {
291 | 		w.Header().Set("Content-Type", "application/json")
292 | 
293 | 		// Define structure for server tools response
294 | 		type ServerTools struct {
295 | 			Server MCPServerInfo `json:"server"`
296 | 			Tools  []mcp.Tool    `json:"tools"`
297 | 		}
298 | 		var serverToolsList []ServerTools
299 | 
300 | 		for _, server := range mcpServers {
301 | 			client, ok := clients[server.Name]
302 | 			if !ok {
303 | 				continue
304 | 			}
305 | 
306 | 			ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
307 | 			defer cancel()
308 | 
309 | 			listReq := mcp.ListToolsRequest{}
310 | 			listResp, err := client.ListTools(ctx, listReq)
311 | 			if err != nil {
312 | 				log.Printf("Error listing tools for %s: %v", server.Name, err)
313 | 				continue
314 | 			}
315 | 
316 | 			serverToolsList = append(serverToolsList, ServerTools{
317 | 				Server: server,
318 | 				Tools:  listResp.Tools,
319 | 			})
320 | 		}
321 | 
322 | 		json.NewEncoder(w).Encode(serverToolsList)
323 | 	}
324 | }
325 | 
326 | // handleOpenAPISpec generates and returns an OpenAPI specification for the server's endpoints
327 | func handleOpenAPISpec(w http.ResponseWriter, r *http.Request) {
328 | 	// Get the domain from environment
329 | 	domain := os.Getenv("NGROK_DOMAIN")
330 | 	if domain == "" {
331 | 		http.Error(w, "NGROK_DOMAIN environment variable not set", http.StatusInternalServerError)
332 | 		return
333 | 	}
334 | 
335 | 	// Build the schema for MCPServerInfo
336 | 	mcpServerInfoSchema := spec.Schema{
337 | 		SchemaProps: spec.SchemaProps{
338 | 			Type: []string{"object"},
339 | 			Properties: map[string]spec.Schema{
340 | 				"name":    {SchemaProps: spec.SchemaProps{Type: []string{"string"}, Description: "Name of the MCP server"}},
341 | 				"command": {SchemaProps: spec.SchemaProps{Type: []string{"string"}, Description: "Command to start the MCP server"}},
342 | 				"env": {
343 | 					SchemaProps: spec.SchemaProps{
344 | 						Type: []string{"array"},
345 | 						Items: &spec.SchemaOrArray{
346 | 							Schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"string"}}},
347 | 						},
348 | 						Description: "Environment variables for the MCP server",
349 | 					},
350 | 				},
351 | 				"args": {
352 | 					SchemaProps: spec.SchemaProps{
353 | 						Type: []string{"array"},
354 | 						Items: &spec.SchemaOrArray{
355 | 							Schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"string"}}},
356 | 						},
357 | 						Description: "Command-line arguments for the MCP server",
358 | 					},
359 | 				},
360 | 			},
361 | 			Required: []string{"name", "command", "env", "args"},
362 | 		},
363 | 	}
364 | 
365 | 	// Build the schema for a Tool
366 | 	toolSchema := spec.Schema{
367 | 		SchemaProps: spec.SchemaProps{
368 | 			Type: []string{"object"},
369 | 			Properties: map[string]spec.Schema{
370 | 				"name":    {SchemaProps: spec.SchemaProps{Type: []string{"string"}, Description: "Name of the tool"}},
371 | 				"command": {SchemaProps: spec.SchemaProps{Type: []string{"string"}, Description: "Command to execute the tool"}},
372 | 				"env": {
373 | 					SchemaProps: spec.SchemaProps{
374 | 						Type: []string{"array"},
375 | 						Items: &spec.SchemaOrArray{
376 | 							Schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"string"}}},
377 | 						},
378 | 						Description: "Environment variables for the tool",
379 | 					},
380 | 				},
381 | 				"args": {
382 | 					SchemaProps: spec.SchemaProps{
383 | 						Type: []string{"array"},
384 | 						Items: &spec.SchemaOrArray{
385 | 							Schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"string"}}},
386 | 						},
387 | 						Description: "Command-line arguments for the tool",
388 | 					},
389 | 				},
390 | 			},
391 | 			Description:          "Details of a tool. Additional properties may be included.",
392 | 			AdditionalProperties: &spec.SchemaOrBool{Allows: true},
393 | 		},
394 | 	}
395 | 
396 | 	// ServerTools: an object with "server" and "tools"
397 | 	serverToolsSchema := spec.Schema{
398 | 		SchemaProps: spec.SchemaProps{
399 | 			Type: []string{"object"},
400 | 			Properties: map[string]spec.Schema{
401 | 				"server": mcpServerInfoSchema,
402 | 				"tools": {
403 | 					SchemaProps: spec.SchemaProps{
404 | 						Type: []string{"array"},
405 | 						Items: &spec.SchemaOrArray{
406 | 							Schema: &toolSchema,
407 | 						},
408 | 						Description: "List of tools available on this server",
409 | 					},
410 | 				},
411 | 			},
412 | 		},
413 | 	}
414 | 
415 | 	// For simplicity, let ServerDescription have the same schema as ServerTools
416 | 	serverDescriptionSchema := serverToolsSchema
417 | 
418 | 	// ExecuteToolResponse: an arbitrary JSON object
419 | 	executeToolResponseSchema := spec.Schema{
420 | 		SchemaProps: spec.SchemaProps{
421 | 			Type:                 []string{"object"},
422 | 			AdditionalProperties: &spec.SchemaOrBool{Allows: true},
423 | 			Description:          "Arbitrary JSON object representing the result of tool execution",
424 | 		},
425 | 	}
426 | 
427 | 	// Build the Swagger spec document
428 | 	swaggerSpec := &spec.Swagger{
429 | 		VendorExtensible: spec.VendorExtensible{
430 | 			Extensions: spec.Extensions{
431 | 				"x-servers": []map[string]string{
432 | 					{"url": "https://" + domain},
433 | 				},
434 | 			},
435 | 		},
436 | 		SwaggerProps: spec.SwaggerProps{
437 | 			Swagger: "3.1.0",
438 | 			Info: &spec.Info{
439 | 				InfoProps: spec.InfoProps{
440 | 					Title:       "MCP Tools API",
441 | 					Description: "API for interacting with MCP servers to list tools, retrieve tool details, execute tools, and list registered servers.",
442 | 					Version:     "v1.0.0",
443 | 				},
444 | 			},
445 | 			Schemes: []string{"https"},
446 | 			Paths: &spec.Paths{
447 | 				Paths: map[string]spec.PathItem{
448 | 					"/servers": {
449 | 						PathItemProps: spec.PathItemProps{
450 | 							Get: &spec.Operation{
451 | 								VendorExtensible: spec.VendorExtensible{
452 | 									Extensions: spec.Extensions{
453 | 										"x-openai-inconsequential": "false",
454 | 									},
455 | 								},
456 | 								OperationProps: spec.OperationProps{
457 | 									ID:          "list_servers-tools",
458 | 									Summary:     "List registered servers with tools",
459 | 									Description: "Returns a list of registered MCP servers along with their available tools.",
460 | 									Produces:    []string{"application/json"},
461 | 									Responses: &spec.Responses{
462 | 										ResponsesProps: spec.ResponsesProps{
463 | 											StatusCodeResponses: map[int]spec.Response{
464 | 												200: {
465 | 													ResponseProps: spec.ResponseProps{
466 | 														Description: "List of registered servers with tools",
467 | 														Schema: &spec.Schema{
468 | 															SchemaProps: spec.SchemaProps{
469 | 																Type: spec.StringOrArray{"array"},
470 | 																Items: &spec.SchemaOrArray{
471 | 																	Schema: &spec.Schema{
472 | 																		SchemaProps: spec.SchemaProps{
473 | 																			Ref: spec.MustCreateRef("#/definitions/ServerTools"),
474 | 																		},
475 | 																	},
476 | 																},
477 | 															},
478 | 														},
479 | 													},
480 | 												},
481 | 											},
482 | 											Default: &spec.Response{
483 | 												ResponseProps: spec.ResponseProps{
484 | 													Description: "Error listing servers",
485 | 												},
486 | 											},
487 | 										},
488 | 									},
489 | 								},
490 | 							},
491 | 						},
492 | 					},
493 | 					"/instructions.txt": {
494 | 						PathItemProps: spec.PathItemProps{
495 | 							Get: &spec.Operation{
496 | 								VendorExtensible: spec.VendorExtensible{
497 | 									Extensions: spec.Extensions{
498 | 										"x-openai-inconsequential": "false",
499 | 									},
500 | 								},
501 | 								OperationProps: spec.OperationProps{
502 | 									ID:          "get_instructions",
503 | 									Summary:     "Get instructions",
504 | 									Description: "Returns a plain text description of all servers and their tools, with detailed usage instructions.",
505 | 									Produces:    []string{"text/plain"},
506 | 									Responses: &spec.Responses{
507 | 										ResponsesProps: spec.ResponsesProps{
508 | 											StatusCodeResponses: map[int]spec.Response{
509 | 												200: {
510 | 													ResponseProps: spec.ResponseProps{
511 | 														Description: "Plain text instructions",
512 | 														Schema: &spec.Schema{
513 | 															SchemaProps: spec.SchemaProps{
514 | 																Type: []string{"string"},
515 | 															},
516 | 														},
517 | 													},
518 | 												},
519 | 											},
520 | 											Default: &spec.Response{
521 | 												ResponseProps: spec.ResponseProps{
522 | 													Description: "Error generating instructions",
523 | 												},
524 | 											},
525 | 										},
526 | 									},
527 | 								},
528 | 							},
529 | 						},
530 | 					},
531 | 				},
532 | 			},
533 | 			Definitions: map[string]spec.Schema{
534 | 				"MCPServerInfo":       mcpServerInfoSchema,
535 | 				"Tool":                toolSchema,
536 | 				"ServerTools":         serverToolsSchema,
537 | 				"ServerDescription":   serverDescriptionSchema,
538 | 				"ExecuteToolResponse": executeToolResponseSchema,
539 | 			},
540 | 		},
541 | 	}
542 | 
543 | 	// Dynamically add paths per registered MCP server and its tools
544 | 	for _, server := range mcpServers {
545 | 		// Define path for describing the server
546 | 		swaggerSpec.Paths.Paths["/"+server.Name] = spec.PathItem{
547 | 			PathItemProps: spec.PathItemProps{
548 | 				Get: &spec.Operation{
549 | 					VendorExtensible: spec.VendorExtensible{
550 | 						Extensions: spec.Extensions{
551 | 							"x-openai-inconsequential": "false",
552 | 						},
553 | 					},
554 | 					OperationProps: spec.OperationProps{
555 | 						ID:          "describe_" + server.Name,
556 | 						Summary:     "Return details for MCP server " + server.Name,
557 | 						Description: "Returns details for " + server.Name + " and its tools.",
558 | 						Produces:    []string{"application/json"},
559 | 						Responses: &spec.Responses{
560 | 							ResponsesProps: spec.ResponsesProps{
561 | 								StatusCodeResponses: map[int]spec.Response{
562 | 									200: {
563 | 										ResponseProps: spec.ResponseProps{
564 | 											Description: "Server details",
565 | 											Schema: &spec.Schema{
566 | 												SchemaProps: spec.SchemaProps{
567 | 													Ref: spec.MustCreateRef("#/definitions/ServerDescription"),
568 | 												},
569 | 											},
570 | 										},
571 | 									},
572 | 								},
573 | 								Default: &spec.Response{
574 | 									ResponseProps: spec.ResponseProps{
575 | 										Description: "Error listing tools",
576 | 									},
577 | 								},
578 | 							},
579 | 						},
580 | 					},
581 | 				},
582 | 			},
583 | 		}
584 | 
585 | 		client, ok := clients[server.Name]
586 | 		if !ok {
587 | 			http.Error(w, fmt.Sprintf("MCP client not found for server: %s", server.Name), http.StatusNotFound)
588 | 			return
589 | 		}
590 | 
591 | 		ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
592 | 		defer cancel()
593 | 
594 | 		listReq := mcp.ListToolsRequest{}
595 | 		listResp, err := client.ListTools(ctx, listReq)
596 | 		if err != nil {
597 | 			http.Error(w, "Error listing tools: "+err.Error(), http.StatusInternalServerError)
598 | 			return
599 | 		}
600 | 
601 | 		for _, tool := range listResp.Tools {
602 | 			// IGNORE THIS TO SAVE THE NUMBER OF ENDPOINTS there is a limit on custom gpt to 30 endpoints
603 | 			if false {
604 | 				// Define path for tool details
605 | 				swaggerSpec.Paths.Paths["/"+server.Name+"/"+tool.Name] = spec.PathItem{
606 | 					PathItemProps: spec.PathItemProps{
607 | 						Get: &spec.Operation{
608 | 							VendorExtensible: spec.VendorExtensible{
609 | 								Extensions: spec.Extensions{
610 | 									"x-openai-inconsequential": "false",
611 | 								},
612 | 							},
613 | 							OperationProps: spec.OperationProps{
614 | 								ID:          "help_" + server.Name + "_" + tool.Name,
615 | 								Summary:     "Get details for " + tool.Name,
616 | 								Description: "Returns details for " + tool.Name + ".",
617 | 								Produces:    []string{"application/json"},
618 | 								Responses: &spec.Responses{
619 | 									ResponsesProps: spec.ResponsesProps{
620 | 										StatusCodeResponses: map[int]spec.Response{
621 | 											200: {
622 | 												ResponseProps: spec.ResponseProps{
623 | 													Description: "Tool details",
624 | 													Schema: &spec.Schema{
625 | 														SchemaProps: spec.SchemaProps{
626 | 															Ref: spec.MustCreateRef("#/definitions/Tool"),
627 | 														},
628 | 													},
629 | 												},
630 | 											},
631 | 										},
632 | 										Default: &spec.Response{
633 | 											ResponseProps: spec.ResponseProps{
634 | 												Description: "Error getting tool details",
635 | 											},
636 | 										},
637 | 									},
638 | 								},
639 | 							},
640 | 						},
641 | 					},
642 | 				}
643 | 			}
644 | 
645 | 			// Define path for tool execution
646 | 			swaggerSpec.Paths.Paths["/"+server.Name+"/"+tool.Name] = spec.PathItem{
647 | 				PathItemProps: spec.PathItemProps{
648 | 					Post: &spec.Operation{
649 | 						VendorExtensible: spec.VendorExtensible{
650 | 							Extensions: spec.Extensions{
651 | 								"x-openai-inconsequential": "false",
652 | 							},
653 | 						},
654 | 						OperationProps: spec.OperationProps{
655 | 							ID:          server.Name + "_" + tool.Name,
656 | 							Summary:     "Execute tool " + tool.Name,
657 | 							Description: "Execute tool " + tool.Name + " with the provided parameters",
658 | 							Produces:    []string{"application/json"},
659 | 							Consumes:    []string{"application/json"},
660 | 							Parameters: func() []spec.Parameter {
661 | 								if tool.InputSchema.Properties == nil {
662 | 									return nil
663 | 								}
664 | 								return []spec.Parameter{
665 | 									{
666 | 										ParamProps: spec.ParamProps{
667 | 											Name:        "body",
668 | 											In:          "body",
669 | 											Description: "Input parameters for " + tool.Name,
670 | 											Required:    true,
671 | 											Schema: &spec.Schema{
672 | 												SchemaProps: spec.SchemaProps{
673 | 													Type:                 []string{"object"},
674 | 													AdditionalProperties: &spec.SchemaOrBool{Allows: true},
675 | 													Properties: func() spec.SchemaProperties {
676 | 														properties := make(spec.SchemaProperties)
677 | 														for name, param := range tool.InputSchema.Properties {
678 | 															properties[name] = spec.Schema{
679 | 																SchemaProps: getSchemaProps(name, param, 0),
680 | 															}
681 | 														}
682 | 														return properties
683 | 													}(),
684 | 												},
685 | 											},
686 | 										},
687 | 									},
688 | 								}
689 | 							}(),
690 | 							Responses: &spec.Responses{
691 | 								ResponsesProps: spec.ResponsesProps{
692 | 									StatusCodeResponses: map[int]spec.Response{
693 | 										200: {
694 | 											ResponseProps: spec.ResponseProps{
695 | 												Description: "Tool execution result",
696 | 												Schema: &spec.Schema{
697 | 													SchemaProps: spec.SchemaProps{
698 | 														Ref: spec.MustCreateRef("#/definitions/ExecuteToolResponse"),
699 | 													},
700 | 												},
701 | 											},
702 | 										},
703 | 									},
704 | 									Default: &spec.Response{
705 | 										ResponseProps: spec.ResponseProps{
706 | 											Description: "Error executing tool",
707 | 										},
708 | 									},
709 | 								},
710 | 							},
711 | 						},
712 | 					},
713 | 				},
714 | 			}
715 | 		}
716 | 	}
717 | 
718 | 	// Marshal the spec into indented JSON
719 | 	b, err := json.MarshalIndent(swaggerSpec, "", "  ")
720 | 	if err != nil {
721 | 		http.Error(w, "Error generating spec: "+err.Error(), http.StatusInternalServerError)
722 | 		return
723 | 	}
724 | 	b = bytes.Replace(b, []byte("\"x-servers\""), []byte("\"servers\""), 1)
725 | 
726 | 	// Convert the JSON into a map to modify it
727 | 	var specMap map[string]interface{}
728 | 	if err := json.Unmarshal(b, &specMap); err != nil {
729 | 		http.Error(w, "Error processing spec: "+err.Error(), http.StatusInternalServerError)
730 | 		return
731 | 	}
732 | 
733 | 	// Convert parameters to requestBody for OpenAPI 3.x compliance
734 | 	paths, ok := specMap["paths"].(map[string]interface{})
735 | 	if ok {
736 | 		// Loop over all paths
737 | 		for _, pathItem := range paths {
738 | 			pathMap, ok := pathItem.(map[string]interface{})
739 | 			if !ok {
740 | 				continue
741 | 			}
742 | 			// Loop over all operations (get, post, etc.)
743 | 			for _, op := range pathMap {
744 | 				opMap, ok := op.(map[string]interface{})
745 | 				if !ok {
746 | 					continue
747 | 				}
748 | 				// Look for parameters
749 | 				if params, exists := opMap["parameters"]; exists {
750 | 					paramArray, ok := params.([]interface{})
751 | 					if !ok {
752 | 						continue
753 | 					}
754 | 					for i, param := range paramArray {
755 | 						paramMap, ok := param.(map[string]interface{})
756 | 						if !ok {
757 | 							continue
758 | 						}
759 | 						// Find the body parameter
760 | 						if in, exists := paramMap["in"]; exists && in == "body" {
761 | 							// Create a requestBody field
762 | 							opMap["requestBody"] = map[string]interface{}{
763 | 								"description": paramMap["description"],
764 | 								"required":    paramMap["required"],
765 | 								"content": map[string]interface{}{
766 | 									"application/json": map[string]interface{}{
767 | 										"schema": paramMap["schema"],
768 | 									},
769 | 								},
770 | 							}
771 | 							// Remove the body parameter from the parameters array
772 | 							paramArray = append(paramArray[:i], paramArray[i+1:]...)
773 | 							break // Assuming only one body parameter exists
774 | 						}
775 | 					}
776 | 					if len(paramArray) > 0 {
777 | 						opMap["parameters"] = paramArray
778 | 					} else {
779 | 						delete(opMap, "parameters")
780 | 					}
781 | 				}
782 | 			}
783 | 		}
784 | 	}
785 | 
786 | 	// Marshal the modified spec back into JSON
787 | 	b, err = json.MarshalIndent(specMap, "", "  ")
788 | 	if err != nil {
789 | 		http.Error(w, "Error generating final spec: "+err.Error(), http.StatusInternalServerError)
790 | 		return
791 | 	}
792 | 
793 | 	w.Header().Set("Content-Type", "application/json")
794 | 	w.Write(b)
795 | }
796 | 
797 | // getTypeParam extracts the type from a parameter
798 | func getTypeParam(param interface{}) string {
799 | 	paramMap, ok := param.(string)
800 | 	if ok {
801 | 		return paramMap
802 | 	}
803 | 	if mm, ok := param.(map[string]interface{}); ok {
804 | 		if mm == nil {
805 | 			return "object"
806 | 		}
807 | 		if type_, ok := mm["type"]; ok {
808 | 			ss, ok := type_.(string)
809 | 			if ok {
810 | 				return ss
811 | 			}
812 | 		}
813 | 		return "object"
814 | 	}
815 | 
816 | 	spew.Dump(param)
817 | 	log.Printf("Unrecognized parameter type: %T", param)
818 | 	return "object" // Default to object for unknown types
819 | }
820 | 
821 | // Maximum recursion depth for schema processing
822 | const maxSchemaDepth = 10
823 | 
824 | // getSchemaProps recursively builds schema properties for OpenAPI spec
825 | func getSchemaProps(name string, param any, depth int) spec.SchemaProps {
826 | 	if depth > maxSchemaDepth {
827 | 		log.Printf("Warning: Schema depth exceeded for %s, limiting recursion", name)
828 | 		res := spec.SchemaProps{
829 | 			Type:  []string{"object"},
830 | 			Title: name,
831 | 		}
832 | 		res.Properties = make(map[string]spec.Schema)
833 | 		if m, ok := param.(map[string]any); ok {
834 | 			for k, v := range m {
835 | 				res.Properties[k] = spec.Schema{
836 | 					SchemaProps: getSchemaProps(k, v, depth+1),
837 | 				}
838 | 			}
839 | 		}
840 | 		return res
841 | 	}
842 | 
843 | 	type_ := getTypeParam(param)
844 | 	res := spec.SchemaProps{
845 | 		Type:  []string{type_},
846 | 		Title: name,
847 | 	}
848 | 
849 | 	if type_ == "object" {
850 | 		res.Properties = make(map[string]spec.Schema)
851 | 		if m, ok := param.(map[string]any); ok {
852 | 			for k, v := range m {
853 | 				res.Properties[k] = spec.Schema{
854 | 					SchemaProps: getSchemaProps(k, v, depth+1),
855 | 				}
856 | 			}
857 | 		}
858 | 	} else if type_ == "array" {
859 | 		if paramMap, ok := param.(map[string]interface{}); ok {
860 | 			if itemsVal, ok := paramMap["items"]; ok {
861 | 				res.Items = &spec.SchemaOrArray{
862 | 					Schema: &spec.Schema{
863 | 						SchemaProps: getSchemaProps(name+"_items", itemsVal, depth+1),
864 | 					},
865 | 				}
866 | 			}
867 | 		}
868 | 	}
869 | 
870 | 	return res
871 | }
872 | 
873 | // handleInstructions generates a plain text description of all servers and their tools
874 | func handleInstructions(w http.ResponseWriter, r *http.Request) {
875 | 	var sb strings.Builder
876 | 	sb.WriteString("# Instructions\n")
877 | 	sb.WriteString("You are a helpful assistant that help the user accomplish tasks by leveraging tools to help acomplish the task, learn something, answer questions, plan, create any document.\n")
878 | 	sb.WriteString("Use the tools to obtain the information you need or ask the user. Try to approach the task step by step.\n")
879 | 
880 | 	sb.WriteString("# MCP Tools Available\n\n")
881 | 	sb.WriteString("These are the available MCP tools to be called through the api:\n\n")
882 | 
883 | 	// Loop through all registered servers
884 | 	for serverName, serverInfo := range mcpServers {
885 | 		sb.WriteString(fmt.Sprintf("## Server: %s\n", serverName))
886 | 		sb.WriteString(fmt.Sprintf("Command: %s\n", serverInfo.Command))
887 | 
888 | 		// Get the client for this server
889 | 		client, ok := clients[serverName]
890 | 		if !ok {
891 | 			sb.WriteString("Error: Client not available for this server\n\n")
892 | 			continue
893 | 		}
894 | 
895 | 		// List the tools for this server
896 | 		ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
897 | 		defer cancel()
898 | 
899 | 		listReq := mcp.ListToolsRequest{}
900 | 		listResp, err := client.ListTools(ctx, listReq)
901 | 		if err != nil {
902 | 			sb.WriteString(fmt.Sprintf("Error listing tools: %v\n\n", err))
903 | 			continue
904 | 		}
905 | 
906 | 		sb.WriteString(fmt.Sprintf("\nAvailable tools (%d):\n\n", len(listResp.Tools)))
907 | 
908 | 		// Loop through all tools for this server
909 | 		for _, tool := range listResp.Tools {
910 | 			sb.WriteString(fmt.Sprintf("### Tool: %s\n", tool.Name))
911 | 			sb.WriteString(fmt.Sprintf("Description: %s\n", tool.Description))
912 | 
913 | 			// Add details about input schema if available
914 | 			if tool.InputSchema.Properties != nil && len(tool.InputSchema.Properties) > 0 {
915 | 				sb.WriteString("\nInput Parameters:\n")
916 | 				for paramName, paramDetails := range tool.InputSchema.Properties {
917 | 					paramType := getTypeParam(paramDetails)
918 | 					sb.WriteString(fmt.Sprintf("- %s (%s)\n", paramName, paramType))
919 | 				}
920 | 			}
921 | 			sb.WriteString("\n")
922 | 		}
923 | 		sb.WriteString("\n---\n\n")
924 | 	}
925 | 
926 | 	w.Header().Set("Content-Type", "text/plain")
927 | 	w.Write([]byte(sb.String()))
928 | }
929 | 
930 | func handleIndex(w http.ResponseWriter, r *http.Request) {
931 | 	w.Write([]byte(`open /openapi.json to get the openapi spec to configure your custom GPT`))
932 | }
933 | 
```