# 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 |
```