diff --git a/README.md b/README.md
index e608fa0..3fb2641 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 `service.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 or SOAP specifications. Now you can enable your AI agent to access any API by simply providing its spec file - no additional coding required.
## Table of Contents
@@ -38,7 +38,7 @@ Run the demo yourself: [Running the Weatherbit Example (Step-by-Step)](#running-
## Features
-- **OpenAPI v2 (Swagger) & v3 Support:** Parses standard specification formats.
+- **OpenAPI v2/v3 & WSDL Support:** Parses standard OpenAPI or SOAP WSDL specifications.
- **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/cmd/openapi-mcp/main.go b/cmd/openapi-mcp/main.go
index 4e05368..452a293 100644
--- a/cmd/openapi-mcp/main.go
+++ b/cmd/openapi-mcp/main.go
@@ -29,7 +29,7 @@ func (i *stringSliceFlag) Set(value string) error {
func main() {
// --- Flag Definitions First ---
// Define specPath early so we can use it for .env loading
- specPath := flag.String("spec", "", "Path or URL to the OpenAPI specification file (required)")
+ specPath := flag.String("spec", "", "Path or URL to the OpenAPI or WSDL specification file (required)")
port := flag.Int("port", 8080, "Port to run the MCP server on")
apiKey := flag.String("api-key", "", "Direct API key value")
@@ -126,7 +126,7 @@ func main() {
// --- Call Parser ---
specDoc, version, err := parser.LoadSwagger(cfg.SpecPath)
if err != nil {
- log.Fatalf("Failed to load OpenAPI/Swagger spec: %v", err)
+ log.Fatalf("Failed to load specification: %v", err)
}
log.Printf("Spec type %s loaded successfully from %s.\n", version, cfg.SpecPath)
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/go.mod b/go.mod
index 8258de8..a168220 100644
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,7 @@ require (
github.com/go-openapi/loads v0.22.0
github.com/go-openapi/spec v0.21.0
github.com/google/uuid v1.6.0
+ github.com/hooklift/gowsdl v0.5.0
github.com/joho/godotenv v1.5.1
github.com/stretchr/testify v1.9.0
)
diff --git a/go.sum b/go.sum
index 6058b23..f3f9edf 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,6 @@
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/getkin/kin-openapi v0.131.0 h1:NO2UeHnFKRYhZ8wg6Nyh5Cq7dHk4suQQr72a4pMrDxE=
@@ -26,6 +27,8 @@ github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/hooklift/gowsdl v0.5.0 h1:DE8RevqhGPLchumV/V7OwbCzfJ8lcozFg1uWC/ESCBQ=
+github.com/hooklift/gowsdl v0.5.0/go.mod h1:9kRc402w9Ci/Mek5a1DNgTmU14yPY8fMumxNVvxhis4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@@ -52,6 +55,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
@@ -61,5 +66,6 @@ go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGc
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/pkg/mcp/types.go b/pkg/mcp/types.go
index f15e003..d960a77 100644
--- a/pkg/mcp/types.go
+++ b/pkg/mcp/types.go
@@ -15,6 +15,9 @@ 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"`
+ Namespace string `json:"namespace,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..b14882a 100644
--- a/pkg/parser/parser.go
+++ b/pkg/parser/parser.go
@@ -15,14 +15,17 @@ 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"
+ gowsdl "github.com/hooklift/gowsdl"
)
const (
- VersionV2 = "v2"
- VersionV3 = "v3"
+ VersionV2 = "v2"
+ VersionV3 = "v3"
+ VersionWSDL = "wsdl"
)
// LoadSwagger detects the version and loads an OpenAPI/Swagger specification
@@ -66,48 +69,56 @@ func LoadSwagger(location string) (interface{}, string, error) {
}
}
- // Detect version from data
+ // Attempt JSON detection first
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 _, ok := detector["openapi"]; ok {
- // OpenAPI 3.x
- loader := openapi3.NewLoader()
- loader.IsExternalRefsAllowed = true
- var doc *openapi3.T
- var loadErr error
+ 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 !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 loadErr != nil {
+ return nil, "", fmt.Errorf("failed to load OpenAPI v3 spec from '%s': %w", location, loadErr)
+ }
- if loadErr != nil {
- return nil, "", fmt.Errorf("failed to load OpenAPI v3 spec from '%s': %w", location, loadErr)
+ 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 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 JSON detection fails, attempt WSDL (XML)
+ trimmed := strings.TrimSpace(string(data))
+ if strings.HasPrefix(trimmed, "<") {
+ wsdlDoc, err := wsdl.LoadWSDL(location)
if err != nil {
- return nil, "", fmt.Errorf("failed to load or validate Swagger v2 spec from '%s': %w", location, err)
+ return nil, "", fmt.Errorf("failed to load WSDL spec from '%s': %w", location, err)
}
- return doc.Spec(), VersionV2, nil
- } else {
- return nil, "", fmt.Errorf("failed to detect OpenAPI/Swagger version in '%s': missing 'openapi' or 'swagger' key", location)
+ return wsdlDoc, VersionWSDL, nil
}
+
+ return nil, "", fmt.Errorf("failed to detect OpenAPI/Swagger version in '%s': missing 'openapi' or 'swagger' key", location)
}
// GenerateToolSet converts a loaded spec (v2 or v3) into an MCP ToolSet.
@@ -125,6 +136,12 @@ func GenerateToolSet(specDoc interface{}, version string, cfg *config.Config) (*
return nil, fmt.Errorf("internal error: expected *spec.Swagger for v2 spec, got %T", specDoc)
}
return generateToolSetV2(docV2, cfg)
+ case VersionWSDL:
+ docWSDL, ok := specDoc.(*gowsdl.WSDL)
+ if !ok {
+ return nil, fmt.Errorf("internal error: expected *gowsdl.WSDL for wsdl spec, got %T", specDoc)
+ }
+ return generateToolSetWSDL(docWSDL, cfg)
default:
return nil, fmt.Errorf("unsupported specification version: %s", version)
}
@@ -550,6 +567,73 @@ func generateToolSetV2(doc *spec.Swagger, cfg *config.Config) (*mcp.ToolSet, err
return toolSet, nil
}
+func generateToolSetWSDL(doc *gowsdl.WSDL, cfg *config.Config) (*mcp.ToolSet, error) {
+ toolSet := createBaseToolSet(doc.Name, doc.Doc, cfg)
+ toolSet.Operations = make(map[string]mcp.OperationDetail)
+
+ baseURL := ""
+ path := ""
+ if len(doc.Service) > 0 && len(doc.Service[0].Ports) > 0 {
+ addr := doc.Service[0].Ports[0].SOAPAddress.Location
+ if u, err := url.Parse(addr); err == nil {
+ baseURL = u.Scheme + "://" + u.Host
+ path = u.Path
+ } else {
+ baseURL = strings.TrimSuffix(addr, "/")
+ }
+ }
+
+ msgMap := make(map[string]*gowsdl.WSDLMessage)
+ for _, m := range doc.Messages {
+ msgMap[m.Name] = m
+ }
+
+ namespace := doc.TargetNamespace
+
+ for _, pt := range doc.PortTypes {
+ for _, op := range pt.Operations {
+ toolName := op.Name
+ toolDesc := op.Doc
+
+ msgName := stripNamespace(op.Input.Message)
+ msg := msgMap[msgName]
+ inputSchema := mcp.Schema{Type: "object", Properties: map[string]mcp.Schema{}}
+ params := []mcp.ParameterDetail{}
+ if msg != nil {
+ for _, p := range msg.Parts {
+ inputSchema.Properties[p.Name] = mcp.Schema{Type: "string"}
+ params = append(params, mcp.ParameterDetail{Name: p.Name, In: "body"})
+ }
+ }
+
+ toolSet.Tools = append(toolSet.Tools, mcp.Tool{
+ Name: toolName,
+ Description: toolDesc,
+ InputSchema: inputSchema,
+ })
+
+ toolSet.Operations[toolName] = mcp.OperationDetail{
+ Method: "POST",
+ Path: path,
+ BaseURL: baseURL,
+ Parameters: params,
+ SOAPAction: op.SOAPOperation.SOAPAction,
+ IsSOAP: true,
+ Namespace: namespace,
+ }
+ }
+ }
+
+ return toolSet, nil
+}
+
+func stripNamespace(name string) string {
+ if idx := strings.Index(name, ":"); idx != -1 {
+ return name[idx+1:]
+ }
+ return name
+}
+
func determineBaseURLV2(doc *spec.Swagger, cfg *config.Config) (string, error) {
if cfg.ServerBaseURL != "" {
return strings.TrimSuffix(cfg.ServerBaseURL, "/"), nil
diff --git a/pkg/parser/parser_test.go b/pkg/parser/parser_test.go
index 5313961..d80b6f0 100644
--- a/pkg/parser/parser_test.go
+++ b/pkg/parser/parser_test.go
@@ -396,6 +396,31 @@ const fileV2SpecJSON = `{
}
}`
+// Minimal WSDL
+const minimalWSDL = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`
+
func TestLoadSwagger(t *testing.T) {
tests := []struct {
name string
@@ -421,12 +446,19 @@ func TestLoadSwagger(t *testing.T) {
expectError: false,
expectVersion: VersionV2,
},
+ {
+ name: "Valid WSDL file",
+ content: minimalWSDL,
+ fileName: "service.wsdl",
+ expectError: false,
+ expectVersion: VersionWSDL,
+ },
{
name: "Malformed JSON file",
content: malformedJSON,
fileName: "malformed.json",
expectError: true,
- containsError: "failed to parse JSON",
+ containsError: "failed to detect OpenAPI/Swagger version",
},
{
name: "No version key JSON file",
@@ -469,7 +501,7 @@ func TestLoadSwagger(t *testing.T) {
name: "Malformed JSON URL",
content: malformedJSON,
expectError: true,
- containsError: "failed to parse JSON",
+ containsError: "failed to detect OpenAPI/Swagger version",
isURLTest: true,
handler: func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
diff --git a/pkg/server/server.go b/pkg/server/server.go
index f80038c..325113f 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
)
@@ -691,7 +692,11 @@ 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 {
+ bodyBytes = []byte(wsdl.BuildSOAPEnvelope(operation.Namespace, toolName, toolInput))
+ reqBody = bytes.NewBuffer(bodyBytes)
+ log.Printf("[ExecuteToolCall] SOAP envelope: %s", string(bodyBytes))
+ } else if requestBodyRequired && len(bodyData) > 0 {
var err error
bodyBytes, err = json.Marshal(bodyData)
if err != nil {
@@ -711,9 +716,19 @@ 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")
+ if reqBody != nil {
+ 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")
+ 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..4c7040d
--- /dev/null
+++ b/pkg/wsdl/wsdl.go
@@ -0,0 +1,80 @@
+package wsdl
+
+import (
+ "encoding/xml"
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "strings"
+
+ gowsdl "github.com/hooklift/gowsdl"
+)
+
+// LoadWSDL reads a WSDL file from a local path or URL and unmarshals it
+// into the gowsdl.WSDL structure.
+func LoadWSDL(location string) (*gowsdl.WSDL, error) {
+ loc, err := gowsdl.ParseLocation(location)
+ if err != nil {
+ return nil, err
+ }
+
+ locStr := loc.String()
+ var data []byte
+ if strings.HasPrefix(locStr, "http://") || strings.HasPrefix(locStr, "https://") {
+ resp, err := http.Get(locStr)
+ if err != nil {
+ return nil, fmt.Errorf("failed to fetch WSDL '%s': %w", locStr, err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, fmt.Errorf("failed to fetch WSDL '%s': status %d, body: %s", locStr, resp.StatusCode, string(body))
+ }
+ data, err = io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ b, err := os.ReadFile(locStr)
+ if err != nil {
+ return nil, err
+ }
+ data = b
+ }
+
+ ws := new(gowsdl.WSDL)
+ if err := xml.Unmarshal(data, ws); err != nil {
+ return nil, err
+ }
+ return ws, nil
+}
+
+// BuildSOAPEnvelope creates a simple SOAP 1.1 envelope with the given
+// operation name and parameters. The namespace will be used as the
+// prefix 'tns' if provided.
+func BuildSOAPEnvelope(namespace, operation string, params map[string]interface{}) string {
+ var sb strings.Builder
+ sb.WriteString(``)
+ sb.WriteString(``)
+ sb.WriteString(``)
+ if namespace != "" {
+ sb.WriteString(``)
+ } else {
+ sb.WriteString(`<` + operation + `>`)
+ }
+ for k, v := range params {
+ sb.WriteString("<" + k + ">" + fmt.Sprintf("%v", v) + "" + k + ">")
+ }
+ if namespace != "" {
+ sb.WriteString(``)
+ } else {
+ sb.WriteString(`` + operation + `>`)
+ }
+ sb.WriteString(``)
+ return sb.String()
+}