diff --git a/README.md b/README.md
index e608fa0..7a09523 100644
--- a/README.md
+++ b/README.md
@@ -7,9 +7,9 @@

-**Generate MCP tool definitions directly from a Swagger/OpenAPI specification file.**
+**Generate MCP tool definitions directly from a Swagger/OpenAPI or WSDL specification file.**
-OpenAPI-MCP is a dockerized MCP server that reads a `swagger.json` or `openapi.yaml` file and generates a corresponding [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) toolset. This allows MCP-compatible clients like [Cursor](https://cursor.sh/) to interact with APIs described by standard OpenAPI specifications. Now you can enable your AI agent to access any API by simply providing its OpenAPI/Swagger specification - no additional coding required.
+OpenAPI-MCP is a dockerized MCP server that reads a `swagger.json`, `openapi.yaml`, or `.wsdl` file and generates a corresponding [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) toolset. This allows MCP-compatible clients like [Cursor](https://cursor.sh/) to interact with APIs described by standard OpenAPI specifications or SOAP services.
## Table of Contents
@@ -38,7 +38,8 @@ Run the demo yourself: [Running the Weatherbit Example (Step-by-Step)](#running-
## Features
-- **OpenAPI v2 (Swagger) & v3 Support:** Parses standard specification formats.
+ - **OpenAPI v2 (Swagger) & v3 Support:** Parses standard specification formats.
+ - **WSDL Support:** Basic conversion of SOAP WSDL operations into MCP tools.
- **Schema Generation:** Creates MCP tool schemas from OpenAPI operation parameters and request/response definitions.
- **Secure API Key Management:**
- Injects API keys into requests (`header`, `query`, `path`, `cookie`) based on command-line configuration.
diff --git a/example/wsdl/tempconvert.wsdl b/example/wsdl/tempconvert.wsdl
new file mode 100644
index 0000000..e80f08e
--- /dev/null
+++ b/example/wsdl/tempconvert.wsdl
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pkg/mcp/types.go b/pkg/mcp/types.go
index f15e003..aa984bc 100644
--- a/pkg/mcp/types.go
+++ b/pkg/mcp/types.go
@@ -15,6 +15,8 @@ type OperationDetail struct {
Path string `json:"path"` // Path template (e.g., /users/{id})
BaseURL string `json:"baseUrl"`
Parameters []ParameterDetail `json:"parameters,omitempty"`
+ SOAPAction string `json:"soapAction,omitempty"`
+ IsSOAP bool `json:"isSoap,omitempty"`
// Add RequestBody schema if needed
}
diff --git a/pkg/parser/parser.go b/pkg/parser/parser.go
index 0ab6cca..385284d 100644
--- a/pkg/parser/parser.go
+++ b/pkg/parser/parser.go
@@ -3,6 +3,7 @@ package parser
import (
"context"
"encoding/json"
+ "encoding/xml"
"fmt"
"io"
"log"
@@ -15,14 +16,16 @@ import (
"github.com/ckanthony/openapi-mcp/pkg/config"
"github.com/ckanthony/openapi-mcp/pkg/mcp"
+ "github.com/ckanthony/openapi-mcp/pkg/wsdl"
"github.com/getkin/kin-openapi/openapi3"
"github.com/go-openapi/loads"
"github.com/go-openapi/spec"
)
const (
- VersionV2 = "v2"
- VersionV3 = "v3"
+ VersionV2 = "v2"
+ VersionV3 = "v3"
+ VersionWSDL = "wsdl"
)
// LoadSwagger detects the version and loads an OpenAPI/Swagger specification
@@ -68,46 +71,71 @@ func LoadSwagger(location string) (interface{}, string, error) {
// Detect version from data
var detector map[string]interface{}
- if err := json.Unmarshal(data, &detector); err != nil {
- return nil, "", fmt.Errorf("failed to parse JSON from '%s' for version detection: %w", location, err)
- }
+ if err := json.Unmarshal(data, &detector); err == nil {
+ if _, ok := detector["openapi"]; ok {
+ // OpenAPI 3.x
+ loader := openapi3.NewLoader()
+ loader.IsExternalRefsAllowed = true
+ var doc *openapi3.T
+ var loadErr error
+
+ if !isURL {
+ // Use LoadFromFile for local files
+ log.Printf("Loading V3 spec using LoadFromFile: %s", absPath)
+ doc, loadErr = loader.LoadFromFile(absPath)
+ } else {
+ // Use LoadFromURI for URLs
+ log.Printf("Loading V3 spec using LoadFromURI: %s", location)
+ doc, loadErr = loader.LoadFromURI(locationURL)
+ }
- if _, ok := detector["openapi"]; ok {
- // OpenAPI 3.x
- loader := openapi3.NewLoader()
- loader.IsExternalRefsAllowed = true
- var doc *openapi3.T
- var loadErr error
+ if loadErr != nil {
+ return nil, "", fmt.Errorf("failed to load OpenAPI v3 spec from '%s': %w", location, loadErr)
+ }
- if !isURL {
- // Use LoadFromFile for local files
- log.Printf("Loading V3 spec using LoadFromFile: %s", absPath)
- doc, loadErr = loader.LoadFromFile(absPath)
- } else {
- // Use LoadFromURI for URLs
- log.Printf("Loading V3 spec using LoadFromURI: %s", location)
- doc, loadErr = loader.LoadFromURI(locationURL)
+ if err := doc.Validate(context.Background()); err != nil {
+ return nil, "", fmt.Errorf("OpenAPI v3 spec validation failed for '%s': %w", location, err)
+ }
+ return doc, VersionV3, nil
+ } else if _, ok := detector["swagger"]; ok {
+ // Swagger 2.0 - Still load from data as loads.Analyzed expects bytes
+ log.Printf("Loading V2 spec using loads.Analyzed from data (source: %s)", location)
+ doc, err := loads.Analyzed(data, "2.0")
+ if err != nil {
+ return nil, "", fmt.Errorf("failed to load or validate Swagger v2 spec from '%s': %w", location, err)
+ }
+ return doc.Spec(), VersionV2, nil
}
-
- if loadErr != nil {
- return nil, "", fmt.Errorf("failed to load OpenAPI v3 spec from '%s': %w", location, loadErr)
+ } else {
+ // Failed to parse JSON - maybe this is a WSDL?
+ if strings.Contains(string(data), " 0 && len(defs.Services[0].Ports) > 0 {
+ addr := defs.Services[0].Ports[0].Address.Location
+ if addr != "" {
+ if u, err := url.Parse(addr); err == nil {
+ baseURL = fmt.Sprintf("%s://%s", u.Scheme, u.Host)
+ path = u.Path
+ if path == "" {
+ path = "/"
+ }
+ }
+ }
+ }
+
+ msgMap := make(map[string]wsdl.Message)
+ for _, m := range defs.Messages {
+ msgMap[m.Name] = m
+ }
+
+ for _, pt := range defs.PortTypes {
+ for _, op := range pt.Operations {
+ msg, ok := msgMap[trimPrefix(op.Input.Message)]
+ if !ok {
+ continue
+ }
+
+ schema := mcp.Schema{Type: "object", Properties: map[string]mcp.Schema{}}
+ for _, p := range msg.Parts {
+ schema.Properties[p.Name] = mcp.Schema{Type: "string"}
+ }
+
+ tool := mcp.Tool{Name: op.Name, Description: op.Documentation, InputSchema: schema}
+ toolSet.Tools = append(toolSet.Tools, tool)
+
+ soapAction := ""
+ for _, b := range defs.Bindings {
+ for _, bop := range b.Operations {
+ if bop.Name == op.Name {
+ soapAction = bop.SoapAction
+ break
+ }
+ }
+ }
+
+ toolSet.Operations[op.Name] = mcp.OperationDetail{
+ Method: http.MethodPost,
+ Path: path,
+ BaseURL: strings.TrimSuffix(baseURL, "/"),
+ Parameters: []mcp.ParameterDetail{},
+ SOAPAction: soapAction,
+ IsSOAP: true,
+ }
+ }
+ }
+
+ return toolSet, nil
+}
+
func determineBaseURLV2(doc *spec.Swagger, cfg *config.Config) (string, error) {
if cfg.ServerBaseURL != "" {
return strings.TrimSuffix(cfg.ServerBaseURL, "/"), nil
@@ -976,3 +1073,10 @@ func sliceContains(slice []string, item string) bool {
}
return false
}
+
+func trimPrefix(name string) string {
+ if idx := strings.Index(name, ":"); idx != -1 {
+ return name[idx+1:]
+ }
+ return name
+}
diff --git a/pkg/server/server.go b/pkg/server/server.go
index f80038c..de66240 100644
--- a/pkg/server/server.go
+++ b/pkg/server/server.go
@@ -18,6 +18,7 @@ import (
"github.com/ckanthony/openapi-mcp/pkg/config"
"github.com/ckanthony/openapi-mcp/pkg/mcp"
+ "github.com/ckanthony/openapi-mcp/pkg/wsdl"
"github.com/google/uuid" // Import UUID package
)
@@ -568,7 +569,10 @@ func executeToolCall(params *ToolCallParams, toolSet *mcp.ToolSet, cfg *config.C
cookieParams = append(cookieParams, clientCookies...)
}
bodyData := make(map[string]interface{}) // For building the request body
- requestBodyRequired := operation.Method == "POST" || operation.Method == "PUT" || operation.Method == "PATCH"
+ requestBodyRequired := operation.Method == http.MethodPost || operation.Method == http.MethodPut || operation.Method == http.MethodPatch
+ if operation.IsSOAP {
+ requestBodyRequired = true
+ }
// Create a map of expected parameters from the operation details for easier lookup
expectedParams := make(map[string]string) // Map param name to its location ('in')
@@ -691,7 +695,12 @@ func executeToolCall(params *ToolCallParams, toolSet *mcp.ToolSet, cfg *config.C
// --- Prepare Request Body ---
var reqBody io.Reader
var bodyBytes []byte // Keep for logging
- if requestBodyRequired && len(bodyData) > 0 {
+ if operation.IsSOAP {
+ envelope := wsdl.BuildSOAPEnvelope(params.ToolName, toolInput)
+ bodyBytes = []byte(envelope)
+ reqBody = bytes.NewBuffer(bodyBytes)
+ log.Printf("[ExecuteToolCall] SOAP Envelope: %s", envelope)
+ } else if requestBodyRequired && len(bodyData) > 0 {
var err error
bodyBytes, err = json.Marshal(bodyData)
if err != nil {
@@ -711,9 +720,17 @@ func executeToolCall(params *ToolCallParams, toolSet *mcp.ToolSet, cfg *config.C
// --- Set Headers ---
// Default headers
- req.Header.Set("Accept", "application/json") // Assume JSON response typical for APIs
- if reqBody != nil {
- req.Header.Set("Content-Type", "application/json") // Assume JSON body if body exists
+ if operation.IsSOAP {
+ req.Header.Set("Accept", "text/xml")
+ req.Header.Set("Content-Type", "text/xml; charset=utf-8")
+ if operation.SOAPAction != "" {
+ req.Header.Set("SOAPAction", operation.SOAPAction)
+ }
+ } else {
+ req.Header.Set("Accept", "application/json") // Assume JSON response typical for APIs
+ if reqBody != nil {
+ req.Header.Set("Content-Type", "application/json")
+ }
}
// Add headers collected from input/spec AND potentially injected API key
diff --git a/pkg/wsdl/wsdl.go b/pkg/wsdl/wsdl.go
new file mode 100644
index 0000000..2a1c15f
--- /dev/null
+++ b/pkg/wsdl/wsdl.go
@@ -0,0 +1,118 @@
+package wsdl
+
+import (
+ "encoding/xml"
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "strings"
+)
+
+// Definitions represents the top-level WSDL definitions element.
+type Definitions struct {
+ XMLName xml.Name `xml:"definitions"`
+ Name string `xml:"name,attr"`
+ TargetNamespace string `xml:"targetNamespace,attr"`
+ Messages []Message `xml:"message"`
+ PortTypes []PortType `xml:"portType"`
+ Bindings []Binding `xml:"binding"`
+ Services []Service `xml:"service"`
+}
+
+type Message struct {
+ Name string `xml:"name,attr"`
+ Parts []Part `xml:"part"`
+}
+
+type Part struct {
+ Name string `xml:"name,attr"`
+ Element string `xml:"element,attr"`
+ Type string `xml:"type,attr"`
+}
+
+type PortType struct {
+ Name string `xml:"name,attr"`
+ Operations []PortTypeOperation `xml:"operation"`
+}
+
+type PortTypeOperation struct {
+ Name string `xml:"name,attr"`
+ Documentation string `xml:"documentation"`
+ Input OperationMessage `xml:"input"`
+ Output OperationMessage `xml:"output"`
+}
+
+type OperationMessage struct {
+ Message string `xml:"message,attr"`
+}
+
+type Binding struct {
+ Name string `xml:"name,attr"`
+ Type string `xml:"type,attr"`
+ Operations []BindingOperation `xml:"operation"`
+}
+
+type BindingOperation struct {
+ Name string `xml:"name,attr"`
+ SoapAction string `xml:"operation>soapAction,attr"`
+}
+
+type Service struct {
+ Name string `xml:"name,attr"`
+ Ports []ServicePort `xml:"port"`
+}
+
+type ServicePort struct {
+ Name string `xml:"name,attr"`
+ Binding string `xml:"binding,attr"`
+ Address Address `xml:"address"`
+}
+
+type Address struct {
+ Location string `xml:"location,attr"`
+}
+
+// LoadWSDL loads a WSDL file from the provided location which can be a local path or a URL.
+func LoadWSDL(location string) (*Definitions, error) {
+ var reader io.ReadCloser
+ var err error
+ if strings.HasPrefix(location, "http://") || strings.HasPrefix(location, "https://") {
+ u, errURL := http.Get(location)
+ if errURL != nil {
+ return nil, errURL
+ }
+ reader = u.Body
+ } else {
+ reader, err = os.Open(location)
+ if err != nil {
+ return nil, err
+ }
+ }
+ defer reader.Close()
+
+ data, err := io.ReadAll(reader)
+ if err != nil {
+ return nil, err
+ }
+ var defs Definitions
+ if err := xml.Unmarshal(data, &defs); err != nil {
+ return nil, err
+ }
+ return &defs, nil
+}
+
+// BuildSOAPEnvelope creates a basic SOAP 1.1 envelope for the given operation and parameters.
+func BuildSOAPEnvelope(operation string, params map[string]interface{}) string {
+ var b strings.Builder
+ b.WriteString(``)
+ b.WriteString(``)
+ b.WriteString(``)
+ b.WriteString(`<` + operation + `>`)
+ for k, v := range params {
+ b.WriteString(`<` + k + `>` + fmt.Sprintf("%v", v) + `` + k + `>`)
+ }
+ b.WriteString(`` + operation + `>`)
+ b.WriteString(``)
+ return b.String()
+}