# Directory Structure
```
├── config.go
├── go.mod
├── go.sum
├── main.go
└── README.md
```
# Files
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# GPT MCP Proxy
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.
This is very useful for integrating MCP tools with custom GPT through Actions.
## Features
- List available MCP servers and their tools
- Get detailed information about specific tools
- Execute tools with custom parameters
- OpenAPI 3.1.0 specification
- Automatic public HTTPS exposure via ngrok
## Prerequisites
- Go 1.20 or later
- ngrok account and authtoken
- MCP-compliant tools
## Configuration
The server requires the following environment variables:
```bash
NGROK_AUTH_TOKEN=your_ngrok_auth_token
NGROK_DOMAIN=your_ngrok_domain
MCP_CONFIG_FILE=/path/to/mcp_settings.json # Optional, defaults to mcp_settings.json
```
### Configuration File Format
Create an `mcp_settings.json` file with your MCP server configurations:
```json
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/Users/username/Desktop",
"/path/to/other/allowed/dir"
]
}
}
}
```
## API Endpoints
- `GET /openapi.json` - OpenAPI specification
- `GET /mcp/servers` - List all servers and their tools
- `GET /mcp/{serverName}` - Get server details
- `GET /mcp/{serverName}/tools/{toolName}` - Get tool details
- `POST /mcp/{serverName}/tools/{toolName}/execute` - Execute a tool
## Usage
1. Set up environment variables
2. Prepare configuration file
3. Run the server:
```bash
go run main.go
```
## Development
To build from source:
```bash
git clone https://github.com/wricardo/mcp-http-server.git
cd mcp-http-server
go build
```
## License
MIT License
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
```
--------------------------------------------------------------------------------
/config.go:
--------------------------------------------------------------------------------
```go
package main
import (
"encoding/json"
"io/ioutil"
"os"
)
// MCPServerConfig represents the configuration for an MCP server.
type MCPServerConfig struct {
Command string `json:"command"`
Args []string `json:"args"`
Env map[string]string `json:"env"`
Disabled bool `json:"disabled"`
AutoApprove []string `json:"autoApprove"`
Timeout int `json:"timeout,omitempty"`
}
// AppConfig represents the overall configuration for the application.
type AppConfig struct {
MCPServers map[string]MCPServerConfig `json:"mcpServers"`
}
// LoadConfig reads the configuration from a file and returns an AppConfig struct.
func LoadConfig(filename string) (*AppConfig, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
bytes, err := ioutil.ReadAll(file)
if err != nil {
return nil, err
}
var config AppConfig
if err := json.Unmarshal(bytes, &config); err != nil {
return nil, err
}
return &config, nil
}
```
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
```go
// Package main provides an HTTP server for MCP (sulti-Tool Coordination Protocol) tools
// It exposes REST endpoints that allow listing, describing, and executing MCP tools
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/go-openapi/spec"
"github.com/gorilla/mux"
mcpclient "github.com/mark3labs/mcp-go/client"
"github.com/mark3labs/mcp-go/mcp"
"golang.ngrok.com/ngrok"
"golang.ngrok.com/ngrok/config"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
func main() {
// Check for required environment variables
ngrokToken := os.Getenv("NGROK_AUTH_TOKEN")
ngrokDomain := os.Getenv("NGROK_DOMAIN")
if ngrokToken == "" || ngrokDomain == "" {
log.Fatal("NGROK_AUTH_TOKEN and NGROK_DOMAIN environment variables must be set")
}
fmt.Println("Using ngrok domain:", ngrokDomain)
fmt.Println("Using ngrok auth token:", ngrokToken)
// Get config file path from environment or use default
configFile := os.Getenv("MCP_CONFIG_FILE")
if configFile == "" {
log.Println("MCP_CONFIG_FILE not set, using default path")
configFile = "mcp_settings.json"
}
// Load configuration from file
cfg, err := LoadConfig(configFile)
if err != nil {
log.Fatalf("Error loading config: %v", err)
}
// Initialize MCP servers registry and clients
mcpServers = make(map[string]MCPServerInfo)
clients = make(map[string]mcpclient.MCPClient)
// Register MCP servers from config
for name, serverConfig := range cfg.MCPServers {
if serverConfig.Disabled {
continue
}
mcpServers[name] = MCPServerInfo{
Name: name,
Command: serverConfig.Command,
Env: convertMapToSlice(serverConfig.Env),
Args: serverConfig.Args,
}
}
// Initialize MCP clients for each server
for name, info := range mcpServers {
client, err := mcpclient.NewStdioMCPClient(info.Command, info.Env, info.Args...)
if err != nil {
log.Fatalf("Error initializing MCP client for %s: %v", name, err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err = client.Initialize(ctx, mcp.InitializeRequest{})
if err != nil {
log.Fatalf("Error initializing MCP client for %s: %v", name, err)
}
log.Printf("Successfully initialized MCP client for %s", name)
clients[name] = client
}
// Set up HTTP routes using Gorilla Mux
router := mux.NewRouter()
router.HandleFunc("/", handleIndex).Methods("GET")
router.HandleFunc("/openapi.json", handleOpenAPISpec).Methods("GET")
router.HandleFunc("/instructions.txt", handleInstructions).Methods("GET")
router.HandleFunc("/servers", listServersToolsHandler(mcpServers)).Methods("GET")
router.HandleFunc("/{serverName}", describeServerHandler).Methods("GET")
router.HandleFunc("/{serverName}/{toolName}", getToolDetailsHandler).Methods("GET")
router.HandleFunc("/{serverName}/{toolName}", executeToolHandler).Methods("POST")
// Start the server using ngrok for public exposure
ctx := context.Background()
listener, err := ngrok.Listen(ctx,
config.HTTPEndpoint(config.WithDomain(ngrokDomain)),
ngrok.WithAuthtokenFromEnv(),
)
if err != nil {
log.Fatal(err)
}
log.Printf("MCP HTTP server is running at https://%s", ngrokDomain)
err = http.Serve(listener, h2c.NewHandler(router, &http2.Server{}))
if err != nil {
log.Fatalf("Server error: %v", err)
}
}
// MCPServerInfo represents a registered MCP server configuration
type MCPServerInfo struct {
Name string `json:"name"`
Command string `json:"command"`
Env []string `json:"env"`
Args []string `json:"args"`
}
// Global registry of MCP servers
var mcpServers map[string]MCPServerInfo
// Global map of initialized MCP clients
var clients map[string]mcpclient.MCPClient
// ToolExecutionRequest represents the payload for executing a tool
type ToolExecutionRequest struct {
InputData string `json:"input_data"`
Config map[string]interface{} `json:"config"`
}
// describeServerHandler queries the MCP server for the details of the server and its tools
func describeServerHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
serverName := vars["serverName"]
client, ok := clients[serverName]
if !ok {
http.Error(w, fmt.Sprintf("MCP client not found for server: %s", serverName), http.StatusNotFound)
return
}
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
listReq := mcp.ListToolsRequest{}
listResp, err := client.ListTools(ctx, listReq)
if err != nil {
http.Error(w, "Error listing tools: "+err.Error(), http.StatusInternalServerError)
return
}
serverInfo, ok := mcpServers[serverName]
if !ok {
http.Error(w, "Server info not found", http.StatusInternalServerError)
return
}
// Define server description structure for response
type ServerDescription struct {
Server MCPServerInfo `json:"server"`
Tools []mcp.Tool `json:"tools"`
}
serverDescription := ServerDescription{
Server: serverInfo,
Tools: listResp.Tools,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(serverDescription)
}
// getToolDetailsHandler returns details for a specified tool
func getToolDetailsHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
serverName := vars["serverName"]
toolName := vars["toolName"]
client, ok := clients[serverName]
if !ok {
http.Error(w, fmt.Sprintf("MCP client not found for server: %s", serverName), http.StatusNotFound)
return
}
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
listReq := mcp.ListToolsRequest{}
listResp, err := client.ListTools(ctx, listReq)
if err != nil {
http.Error(w, "Error listing tools: "+err.Error(), http.StatusInternalServerError)
return
}
var found *mcp.Tool
for _, tool := range listResp.Tools {
if tool.Name == toolName {
found = &tool
break
}
}
if found == nil {
http.Error(w, fmt.Sprintf("Tool %s not found", toolName), http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(found)
}
// executeToolHandler invokes a tool on the MCP server synchronously
func executeToolHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
serverName := vars["serverName"]
toolName := vars["toolName"]
client, ok := clients[serverName]
if !ok {
http.Error(w, fmt.Sprintf("MCP client not found for server: %s", serverName), http.StatusNotFound)
return
}
var args map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest)
return
}
// Use a longer timeout for tool execution
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
// Build the tool call request
callReq := mcp.CallToolRequest{
Request: mcp.Request{
Method: "tools/call",
},
}
callReq.Params.Name = toolName
callReq.Params.Arguments = args
log.Printf("Tool name: %s\n", toolName)
encoded, err := json.Marshal(args)
if err != nil {
log.Printf("Error encoding args: %v\n", err)
}
log.Printf("Args: %s\n", string(encoded))
result, err := client.CallTool(ctx, callReq)
// log the the request and result. Pretty.
if err != nil {
log.Printf("error: %v\n", err)
}
log.Printf("error: %v\n", err)
if result != nil {
if result.IsError {
log.Println("response is an error.")
}
log.Println("Response:")
for _, content := range result.Content {
casted := content.(mcp.TextContent)
fmt.Println(casted.Text)
}
}
if err != nil {
errorMsg := fmt.Sprintf("Error executing tool %s: %v", toolName, err)
http.Error(w, errorMsg, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
}
// convertMapToSlice converts a map of environment variables to a slice of strings in "key=value" format
func convertMapToSlice(envMap map[string]string) []string {
var env []string
for k, v := range envMap {
env = append(env, k+"="+v)
}
return env
}
// listServersToolsHandler returns a handler function that lists all registered servers and their tools
func listServersToolsHandler(mcpServers map[string]MCPServerInfo) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// Define structure for server tools response
type ServerTools struct {
Server MCPServerInfo `json:"server"`
Tools []mcp.Tool `json:"tools"`
}
var serverToolsList []ServerTools
for _, server := range mcpServers {
client, ok := clients[server.Name]
if !ok {
continue
}
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
listReq := mcp.ListToolsRequest{}
listResp, err := client.ListTools(ctx, listReq)
if err != nil {
log.Printf("Error listing tools for %s: %v", server.Name, err)
continue
}
serverToolsList = append(serverToolsList, ServerTools{
Server: server,
Tools: listResp.Tools,
})
}
json.NewEncoder(w).Encode(serverToolsList)
}
}
// handleOpenAPISpec generates and returns an OpenAPI specification for the server's endpoints
func handleOpenAPISpec(w http.ResponseWriter, r *http.Request) {
// Get the domain from environment
domain := os.Getenv("NGROK_DOMAIN")
if domain == "" {
http.Error(w, "NGROK_DOMAIN environment variable not set", http.StatusInternalServerError)
return
}
// Build the schema for MCPServerInfo
mcpServerInfoSchema := spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"name": {SchemaProps: spec.SchemaProps{Type: []string{"string"}, Description: "Name of the MCP server"}},
"command": {SchemaProps: spec.SchemaProps{Type: []string{"string"}, Description: "Command to start the MCP server"}},
"env": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"string"}}},
},
Description: "Environment variables for the MCP server",
},
},
"args": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"string"}}},
},
Description: "Command-line arguments for the MCP server",
},
},
},
Required: []string{"name", "command", "env", "args"},
},
}
// Build the schema for a Tool
toolSchema := spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"name": {SchemaProps: spec.SchemaProps{Type: []string{"string"}, Description: "Name of the tool"}},
"command": {SchemaProps: spec.SchemaProps{Type: []string{"string"}, Description: "Command to execute the tool"}},
"env": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"string"}}},
},
Description: "Environment variables for the tool",
},
},
"args": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"string"}}},
},
Description: "Command-line arguments for the tool",
},
},
},
Description: "Details of a tool. Additional properties may be included.",
AdditionalProperties: &spec.SchemaOrBool{Allows: true},
},
}
// ServerTools: an object with "server" and "tools"
serverToolsSchema := spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"server": mcpServerInfoSchema,
"tools": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &toolSchema,
},
Description: "List of tools available on this server",
},
},
},
},
}
// For simplicity, let ServerDescription have the same schema as ServerTools
serverDescriptionSchema := serverToolsSchema
// ExecuteToolResponse: an arbitrary JSON object
executeToolResponseSchema := spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{Allows: true},
Description: "Arbitrary JSON object representing the result of tool execution",
},
}
// Build the Swagger spec document
swaggerSpec := &spec.Swagger{
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-servers": []map[string]string{
{"url": "https://" + domain},
},
},
},
SwaggerProps: spec.SwaggerProps{
Swagger: "3.1.0",
Info: &spec.Info{
InfoProps: spec.InfoProps{
Title: "MCP Tools API",
Description: "API for interacting with MCP servers to list tools, retrieve tool details, execute tools, and list registered servers.",
Version: "v1.0.0",
},
},
Schemes: []string{"https"},
Paths: &spec.Paths{
Paths: map[string]spec.PathItem{
"/servers": {
PathItemProps: spec.PathItemProps{
Get: &spec.Operation{
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-openai-inconsequential": "false",
},
},
OperationProps: spec.OperationProps{
ID: "list_servers-tools",
Summary: "List registered servers with tools",
Description: "Returns a list of registered MCP servers along with their available tools.",
Produces: []string{"application/json"},
Responses: &spec.Responses{
ResponsesProps: spec.ResponsesProps{
StatusCodeResponses: map[int]spec.Response{
200: {
ResponseProps: spec.ResponseProps{
Description: "List of registered servers with tools",
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: spec.StringOrArray{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Ref: spec.MustCreateRef("#/definitions/ServerTools"),
},
},
},
},
},
},
},
},
Default: &spec.Response{
ResponseProps: spec.ResponseProps{
Description: "Error listing servers",
},
},
},
},
},
},
},
},
"/instructions.txt": {
PathItemProps: spec.PathItemProps{
Get: &spec.Operation{
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-openai-inconsequential": "false",
},
},
OperationProps: spec.OperationProps{
ID: "get_instructions",
Summary: "Get instructions",
Description: "Returns a plain text description of all servers and their tools, with detailed usage instructions.",
Produces: []string{"text/plain"},
Responses: &spec.Responses{
ResponsesProps: spec.ResponsesProps{
StatusCodeResponses: map[int]spec.Response{
200: {
ResponseProps: spec.ResponseProps{
Description: "Plain text instructions",
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
},
},
},
},
},
Default: &spec.Response{
ResponseProps: spec.ResponseProps{
Description: "Error generating instructions",
},
},
},
},
},
},
},
},
},
},
Definitions: map[string]spec.Schema{
"MCPServerInfo": mcpServerInfoSchema,
"Tool": toolSchema,
"ServerTools": serverToolsSchema,
"ServerDescription": serverDescriptionSchema,
"ExecuteToolResponse": executeToolResponseSchema,
},
},
}
// Dynamically add paths per registered MCP server and its tools
for _, server := range mcpServers {
// Define path for describing the server
swaggerSpec.Paths.Paths["/"+server.Name] = spec.PathItem{
PathItemProps: spec.PathItemProps{
Get: &spec.Operation{
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-openai-inconsequential": "false",
},
},
OperationProps: spec.OperationProps{
ID: "describe_" + server.Name,
Summary: "Return details for MCP server " + server.Name,
Description: "Returns details for " + server.Name + " and its tools.",
Produces: []string{"application/json"},
Responses: &spec.Responses{
ResponsesProps: spec.ResponsesProps{
StatusCodeResponses: map[int]spec.Response{
200: {
ResponseProps: spec.ResponseProps{
Description: "Server details",
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Ref: spec.MustCreateRef("#/definitions/ServerDescription"),
},
},
},
},
},
Default: &spec.Response{
ResponseProps: spec.ResponseProps{
Description: "Error listing tools",
},
},
},
},
},
},
},
}
client, ok := clients[server.Name]
if !ok {
http.Error(w, fmt.Sprintf("MCP client not found for server: %s", server.Name), http.StatusNotFound)
return
}
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
listReq := mcp.ListToolsRequest{}
listResp, err := client.ListTools(ctx, listReq)
if err != nil {
http.Error(w, "Error listing tools: "+err.Error(), http.StatusInternalServerError)
return
}
for _, tool := range listResp.Tools {
// IGNORE THIS TO SAVE THE NUMBER OF ENDPOINTS there is a limit on custom gpt to 30 endpoints
if false {
// Define path for tool details
swaggerSpec.Paths.Paths["/"+server.Name+"/"+tool.Name] = spec.PathItem{
PathItemProps: spec.PathItemProps{
Get: &spec.Operation{
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-openai-inconsequential": "false",
},
},
OperationProps: spec.OperationProps{
ID: "help_" + server.Name + "_" + tool.Name,
Summary: "Get details for " + tool.Name,
Description: "Returns details for " + tool.Name + ".",
Produces: []string{"application/json"},
Responses: &spec.Responses{
ResponsesProps: spec.ResponsesProps{
StatusCodeResponses: map[int]spec.Response{
200: {
ResponseProps: spec.ResponseProps{
Description: "Tool details",
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Ref: spec.MustCreateRef("#/definitions/Tool"),
},
},
},
},
},
Default: &spec.Response{
ResponseProps: spec.ResponseProps{
Description: "Error getting tool details",
},
},
},
},
},
},
},
}
}
// Define path for tool execution
swaggerSpec.Paths.Paths["/"+server.Name+"/"+tool.Name] = spec.PathItem{
PathItemProps: spec.PathItemProps{
Post: &spec.Operation{
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-openai-inconsequential": "false",
},
},
OperationProps: spec.OperationProps{
ID: server.Name + "_" + tool.Name,
Summary: "Execute tool " + tool.Name,
Description: "Execute tool " + tool.Name + " with the provided parameters",
Produces: []string{"application/json"},
Consumes: []string{"application/json"},
Parameters: func() []spec.Parameter {
if tool.InputSchema.Properties == nil {
return nil
}
return []spec.Parameter{
{
ParamProps: spec.ParamProps{
Name: "body",
In: "body",
Description: "Input parameters for " + tool.Name,
Required: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{Allows: true},
Properties: func() spec.SchemaProperties {
properties := make(spec.SchemaProperties)
for name, param := range tool.InputSchema.Properties {
properties[name] = spec.Schema{
SchemaProps: getSchemaProps(name, param, 0),
}
}
return properties
}(),
},
},
},
},
}
}(),
Responses: &spec.Responses{
ResponsesProps: spec.ResponsesProps{
StatusCodeResponses: map[int]spec.Response{
200: {
ResponseProps: spec.ResponseProps{
Description: "Tool execution result",
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Ref: spec.MustCreateRef("#/definitions/ExecuteToolResponse"),
},
},
},
},
},
Default: &spec.Response{
ResponseProps: spec.ResponseProps{
Description: "Error executing tool",
},
},
},
},
},
},
},
}
}
}
// Marshal the spec into indented JSON
b, err := json.MarshalIndent(swaggerSpec, "", " ")
if err != nil {
http.Error(w, "Error generating spec: "+err.Error(), http.StatusInternalServerError)
return
}
b = bytes.Replace(b, []byte("\"x-servers\""), []byte("\"servers\""), 1)
// Convert the JSON into a map to modify it
var specMap map[string]interface{}
if err := json.Unmarshal(b, &specMap); err != nil {
http.Error(w, "Error processing spec: "+err.Error(), http.StatusInternalServerError)
return
}
// Convert parameters to requestBody for OpenAPI 3.x compliance
paths, ok := specMap["paths"].(map[string]interface{})
if ok {
// Loop over all paths
for _, pathItem := range paths {
pathMap, ok := pathItem.(map[string]interface{})
if !ok {
continue
}
// Loop over all operations (get, post, etc.)
for _, op := range pathMap {
opMap, ok := op.(map[string]interface{})
if !ok {
continue
}
// Look for parameters
if params, exists := opMap["parameters"]; exists {
paramArray, ok := params.([]interface{})
if !ok {
continue
}
for i, param := range paramArray {
paramMap, ok := param.(map[string]interface{})
if !ok {
continue
}
// Find the body parameter
if in, exists := paramMap["in"]; exists && in == "body" {
// Create a requestBody field
opMap["requestBody"] = map[string]interface{}{
"description": paramMap["description"],
"required": paramMap["required"],
"content": map[string]interface{}{
"application/json": map[string]interface{}{
"schema": paramMap["schema"],
},
},
}
// Remove the body parameter from the parameters array
paramArray = append(paramArray[:i], paramArray[i+1:]...)
break // Assuming only one body parameter exists
}
}
if len(paramArray) > 0 {
opMap["parameters"] = paramArray
} else {
delete(opMap, "parameters")
}
}
}
}
}
// Marshal the modified spec back into JSON
b, err = json.MarshalIndent(specMap, "", " ")
if err != nil {
http.Error(w, "Error generating final spec: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(b)
}
// getTypeParam extracts the type from a parameter
func getTypeParam(param interface{}) string {
paramMap, ok := param.(string)
if ok {
return paramMap
}
if mm, ok := param.(map[string]interface{}); ok {
if mm == nil {
return "object"
}
if type_, ok := mm["type"]; ok {
ss, ok := type_.(string)
if ok {
return ss
}
}
return "object"
}
spew.Dump(param)
log.Printf("Unrecognized parameter type: %T", param)
return "object" // Default to object for unknown types
}
// Maximum recursion depth for schema processing
const maxSchemaDepth = 10
// getSchemaProps recursively builds schema properties for OpenAPI spec
func getSchemaProps(name string, param any, depth int) spec.SchemaProps {
if depth > maxSchemaDepth {
log.Printf("Warning: Schema depth exceeded for %s, limiting recursion", name)
res := spec.SchemaProps{
Type: []string{"object"},
Title: name,
}
res.Properties = make(map[string]spec.Schema)
if m, ok := param.(map[string]any); ok {
for k, v := range m {
res.Properties[k] = spec.Schema{
SchemaProps: getSchemaProps(k, v, depth+1),
}
}
}
return res
}
type_ := getTypeParam(param)
res := spec.SchemaProps{
Type: []string{type_},
Title: name,
}
if type_ == "object" {
res.Properties = make(map[string]spec.Schema)
if m, ok := param.(map[string]any); ok {
for k, v := range m {
res.Properties[k] = spec.Schema{
SchemaProps: getSchemaProps(k, v, depth+1),
}
}
}
} else if type_ == "array" {
if paramMap, ok := param.(map[string]interface{}); ok {
if itemsVal, ok := paramMap["items"]; ok {
res.Items = &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: getSchemaProps(name+"_items", itemsVal, depth+1),
},
}
}
}
}
return res
}
// handleInstructions generates a plain text description of all servers and their tools
func handleInstructions(w http.ResponseWriter, r *http.Request) {
var sb strings.Builder
sb.WriteString("# Instructions\n")
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")
sb.WriteString("Use the tools to obtain the information you need or ask the user. Try to approach the task step by step.\n")
sb.WriteString("# MCP Tools Available\n\n")
sb.WriteString("These are the available MCP tools to be called through the api:\n\n")
// Loop through all registered servers
for serverName, serverInfo := range mcpServers {
sb.WriteString(fmt.Sprintf("## Server: %s\n", serverName))
sb.WriteString(fmt.Sprintf("Command: %s\n", serverInfo.Command))
// Get the client for this server
client, ok := clients[serverName]
if !ok {
sb.WriteString("Error: Client not available for this server\n\n")
continue
}
// List the tools for this server
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
listReq := mcp.ListToolsRequest{}
listResp, err := client.ListTools(ctx, listReq)
if err != nil {
sb.WriteString(fmt.Sprintf("Error listing tools: %v\n\n", err))
continue
}
sb.WriteString(fmt.Sprintf("\nAvailable tools (%d):\n\n", len(listResp.Tools)))
// Loop through all tools for this server
for _, tool := range listResp.Tools {
sb.WriteString(fmt.Sprintf("### Tool: %s\n", tool.Name))
sb.WriteString(fmt.Sprintf("Description: %s\n", tool.Description))
// Add details about input schema if available
if tool.InputSchema.Properties != nil && len(tool.InputSchema.Properties) > 0 {
sb.WriteString("\nInput Parameters:\n")
for paramName, paramDetails := range tool.InputSchema.Properties {
paramType := getTypeParam(paramDetails)
sb.WriteString(fmt.Sprintf("- %s (%s)\n", paramName, paramType))
}
}
sb.WriteString("\n")
}
sb.WriteString("\n---\n\n")
}
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte(sb.String()))
}
func handleIndex(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`open /openapi.json to get the openapi spec to configure your custom GPT`))
}
```