convert.go 28 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117
  1. package convert
  2. import (
  3. "bytes"
  4. "context"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "net/http"
  9. "net/url"
  10. "strings"
  11. "github.com/bytedance/sonic"
  12. "github.com/getkin/kin-openapi/openapi3"
  13. "github.com/mark3labs/mcp-go/mcp"
  14. "github.com/mark3labs/mcp-go/server"
  15. )
  16. type Options struct {
  17. OpenAPIFrom string
  18. ServerName string
  19. Version string
  20. ToolNamePrefix string
  21. ServerAddr string
  22. Authorization string
  23. }
  24. // Converter represents an OpenAPI to MCP converter
  25. type Converter struct {
  26. parser *Parser
  27. options Options
  28. }
  29. // NewConverter creates a new OpenAPI to MCP converter
  30. func NewConverter(parser *Parser, options Options) *Converter {
  31. return &Converter{
  32. parser: parser,
  33. options: options,
  34. }
  35. }
  36. // Convert converts an OpenAPI document to an MCP configuration
  37. func (c *Converter) Convert() (*server.MCPServer, error) {
  38. if c.parser.GetDocument() == nil {
  39. return nil, errors.New("no OpenAPI document loaded")
  40. }
  41. info := c.parser.GetInfo()
  42. if info == nil {
  43. return nil, errors.New("no info found in OpenAPI document")
  44. }
  45. if c.options.ServerName == "" {
  46. c.options.ServerName = info.Title
  47. }
  48. if c.options.Version == "" {
  49. c.options.Version = info.Version
  50. }
  51. // Create the MCP configuration
  52. mcpServer := server.NewMCPServer(
  53. c.options.ServerName,
  54. c.options.Version,
  55. )
  56. defaultServer := c.options.ServerAddr
  57. // Use custom server address if provided
  58. if defaultServer == "" {
  59. servers := c.parser.GetServers()
  60. if len(servers) == 1 {
  61. server, err := getServerURL(c.options.OpenAPIFrom, servers[0].URL)
  62. if err != nil {
  63. return nil, err
  64. }
  65. defaultServer = server
  66. } else if len(servers) == 0 {
  67. defaultServer, _ = getServerURL(c.options.OpenAPIFrom, "")
  68. }
  69. }
  70. // Process each path and operation
  71. for path, pathItem := range c.parser.GetPaths().Map() {
  72. operations := getOperations(pathItem)
  73. for method, operation := range operations {
  74. tool := c.convertOperation(path, method, operation)
  75. handler := newHandler(defaultServer, c.options.Authorization, path, method, operation)
  76. mcpServer.AddTool(*tool, handler)
  77. }
  78. }
  79. return mcpServer, nil
  80. }
  81. func getServerURL(from, dir string) (string, error) {
  82. if from == "" {
  83. return dir, nil
  84. }
  85. if strings.HasPrefix(dir, "http://") ||
  86. strings.HasPrefix(dir, "https://") {
  87. return dir, nil
  88. }
  89. if !strings.HasPrefix(from, "http://") &&
  90. !strings.HasPrefix(from, "https://") {
  91. return dir, nil
  92. }
  93. result, err := url.Parse(from)
  94. if err != nil {
  95. return "", err
  96. }
  97. result.Path = dir
  98. result.RawQuery = ""
  99. return result.String(), nil
  100. }
  101. // TODO: valid operation
  102. func newHandler(
  103. defaultServer, authorization, path, method string,
  104. _ *openapi3.Operation,
  105. ) server.ToolHandlerFunc {
  106. return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  107. arg := getArgs(request.GetArguments())
  108. // Build the URL
  109. serverURL := arg.ServerAddr
  110. if serverURL == "" {
  111. serverURL = defaultServer
  112. }
  113. // Replace path parameters
  114. finalPath := path
  115. for paramName, paramValue := range arg.Path {
  116. finalPath = strings.ReplaceAll(
  117. finalPath,
  118. "{"+paramName+"}",
  119. fmt.Sprintf("%v", paramValue),
  120. )
  121. }
  122. // Build the full URL with query parameters
  123. fullURL, err := url.JoinPath(serverURL, finalPath)
  124. if err != nil {
  125. return nil, fmt.Errorf("failed to join URL path %s: %w", fullURL, err)
  126. }
  127. parsedURL, err := url.Parse(fullURL)
  128. if err != nil {
  129. return nil, fmt.Errorf("failed to parse URL %s: %w", fullURL, err)
  130. }
  131. // Add query parameters
  132. if len(arg.Query) > 0 {
  133. q := parsedURL.Query()
  134. for key, value := range arg.Query {
  135. q.Add(key, fmt.Sprintf("%v", value))
  136. }
  137. parsedURL.RawQuery = q.Encode()
  138. }
  139. // Create the request body if needed
  140. var reqBody io.Reader
  141. if arg.Body != nil {
  142. bodyBytes, err := sonic.Marshal(arg.Body)
  143. if err != nil {
  144. return nil, fmt.Errorf("failed to marshal request body: %w", err)
  145. }
  146. reqBody = bytes.NewBuffer(bodyBytes)
  147. }
  148. // Create the HTTP request
  149. httpReq, err := http.NewRequestWithContext(
  150. ctx,
  151. strings.ToUpper(method),
  152. parsedURL.String(),
  153. reqBody,
  154. )
  155. if err != nil {
  156. return nil, fmt.Errorf("failed to create HTTP request: %w", err)
  157. }
  158. // Add headers
  159. for key, value := range arg.Headers {
  160. httpReq.Header.Add(key, fmt.Sprintf("%v", value))
  161. }
  162. // Set content type for requests with body
  163. if arg.BodyContentType != "" {
  164. httpReq.Header.Set("Content-Type", arg.BodyContentType)
  165. } else if arg.Body != nil {
  166. httpReq.Header.Set("Content-Type", "application/json")
  167. }
  168. // Add authentication if provided
  169. switch {
  170. case authorization != "":
  171. httpReq.Header.Set("Authorization", authorization)
  172. case arg.AuthToken != "":
  173. httpReq.Header.Set("Authorization", "Bearer "+arg.AuthToken)
  174. case arg.AuthUsername != "" && arg.AuthPassword != "":
  175. httpReq.SetBasicAuth(arg.AuthUsername, arg.AuthPassword)
  176. case arg.AuthOAuth2Token != "":
  177. httpReq.Header.Set("Authorization", "Bearer "+arg.AuthOAuth2Token)
  178. }
  179. // For form data
  180. if len(arg.Forms) > 0 {
  181. formData := url.Values{}
  182. for key, value := range arg.Forms {
  183. switch value := value.(type) {
  184. case map[string]any:
  185. jsonStr, err := sonic.Marshal(value)
  186. if err != nil {
  187. return nil, err
  188. }
  189. formData.Add(key, string(jsonStr))
  190. default:
  191. formData.Add(key, fmt.Sprintf("%v", value))
  192. }
  193. }
  194. httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  195. httpReq.Body = io.NopCloser(strings.NewReader(formData.Encode()))
  196. }
  197. resp, err := http.DefaultClient.Do(httpReq)
  198. if err != nil {
  199. return nil, fmt.Errorf("request failed: %w", err)
  200. }
  201. defer resp.Body.Close()
  202. buf := bytes.NewBuffer(nil)
  203. err = resp.Write(buf)
  204. if err != nil {
  205. return nil, fmt.Errorf("read response error: %w", err)
  206. }
  207. return mcp.NewToolResultText(buf.String()), nil
  208. }
  209. }
  210. type Args struct {
  211. ServerAddr string
  212. AuthToken string
  213. AuthUsername string
  214. AuthPassword string
  215. AuthOAuth2Token string
  216. Headers map[string]any
  217. Body any
  218. BodyContentType string
  219. Query map[string]any
  220. Path map[string]any
  221. Forms map[string]any
  222. }
  223. func getArgs(args map[string]any) Args {
  224. arg := Args{
  225. Headers: make(map[string]any),
  226. Query: make(map[string]any),
  227. Path: make(map[string]any),
  228. Forms: make(map[string]any),
  229. }
  230. for k, v := range args {
  231. switch {
  232. case strings.HasPrefix(k, "openapi|"):
  233. switch strings.TrimPrefix(k, "openapi|") {
  234. case "server_addr":
  235. arg.ServerAddr, _ = v.(string)
  236. case "auth_token":
  237. arg.AuthToken, _ = v.(string)
  238. case "auth_username":
  239. arg.AuthUsername, _ = v.(string)
  240. case "auth_password":
  241. arg.AuthPassword, _ = v.(string)
  242. case "auth_oauth2_token":
  243. arg.AuthOAuth2Token, _ = v.(string)
  244. }
  245. case k == "body":
  246. arg.Body = v
  247. case strings.HasPrefix(k, "body|"):
  248. arg.Body = v
  249. arg.BodyContentType = strings.TrimPrefix(k, "body|")
  250. case strings.HasPrefix(k, "query|"):
  251. arg.Query[strings.TrimPrefix(k, "query|")] = v
  252. case strings.HasPrefix(k, "path|"):
  253. arg.Path[strings.TrimPrefix(k, "path|")] = v
  254. case strings.HasPrefix(k, "header|"):
  255. arg.Headers[strings.TrimPrefix(k, "header|")] = v
  256. case strings.HasPrefix(k, "formData|"):
  257. arg.Forms[strings.TrimPrefix(k, "formData|")] = v
  258. }
  259. }
  260. return arg
  261. }
  262. // getOperations returns a map of HTTP method to operation
  263. func getOperations(pathItem *openapi3.PathItem) map[string]*openapi3.Operation {
  264. operations := make(map[string]*openapi3.Operation)
  265. if pathItem.Get != nil {
  266. operations[http.MethodGet] = pathItem.Get
  267. }
  268. if pathItem.Post != nil {
  269. operations[http.MethodPost] = pathItem.Post
  270. }
  271. if pathItem.Put != nil {
  272. operations[http.MethodPut] = pathItem.Put
  273. }
  274. if pathItem.Delete != nil {
  275. operations[http.MethodDelete] = pathItem.Delete
  276. }
  277. if pathItem.Options != nil {
  278. operations[http.MethodOptions] = pathItem.Options
  279. }
  280. if pathItem.Head != nil {
  281. operations[http.MethodHead] = pathItem.Head
  282. }
  283. if pathItem.Patch != nil {
  284. operations[http.MethodPatch] = pathItem.Patch
  285. }
  286. if pathItem.Trace != nil {
  287. operations[http.MethodTrace] = pathItem.Trace
  288. }
  289. return operations
  290. }
  291. // convertOperation converts an OpenAPI operation to an MCP tool
  292. func (c *Converter) convertOperation(path, method string, operation *openapi3.Operation) *mcp.Tool {
  293. // Generate a tool name
  294. toolName := c.parser.GetOperationID(path, method, operation)
  295. if c.options.ToolNamePrefix != "" {
  296. toolName = c.options.ToolNamePrefix + toolName
  297. }
  298. args := c.convertParameters(operation.Parameters)
  299. // Handle request body if present
  300. if operation.RequestBody != nil && operation.RequestBody.Value != nil {
  301. bodyArgs := c.convertRequestBody(operation.RequestBody.Value)
  302. args = append(args, bodyArgs...)
  303. }
  304. // Add server address parameter
  305. servers := c.parser.GetServers()
  306. switch {
  307. case c.options.ServerAddr != "":
  308. // Use custom server address from options
  309. args = append(args, mcp.WithString("openapi|server_addr",
  310. mcp.Description("Server address to connect to"),
  311. mcp.DefaultString(c.options.ServerAddr)))
  312. case len(servers) == 0:
  313. if c.options.OpenAPIFrom != "" {
  314. u, err := getServerURL(c.options.OpenAPIFrom, "")
  315. if err != nil {
  316. u = c.options.OpenAPIFrom
  317. }
  318. args = append(args, mcp.WithString("openapi|server_addr",
  319. mcp.Description("Server address to connect to, example: "+u),
  320. mcp.Required()))
  321. } else {
  322. args = append(args, mcp.WithString("openapi|server_addr",
  323. mcp.Description("Server address to connect to"),
  324. mcp.Required()))
  325. }
  326. case len(servers) == 1:
  327. u, err := getServerURL(c.options.OpenAPIFrom, servers[0].URL)
  328. if err != nil {
  329. u = servers[0].URL
  330. }
  331. args = append(args, mcp.WithString("openapi|server_addr",
  332. mcp.Description("Server address to connect to"),
  333. mcp.DefaultString(u)))
  334. default:
  335. serverUrls := make([]string, 0, len(servers))
  336. for _, server := range servers {
  337. u, err := getServerURL(c.options.OpenAPIFrom, server.URL)
  338. if err != nil {
  339. u = server.URL
  340. }
  341. serverUrls = append(serverUrls, u)
  342. }
  343. args = append(args, mcp.WithString("openapi|server_addr",
  344. mcp.Description("Server address to connect to"),
  345. mcp.Required(),
  346. mcp.Enum(serverUrls...)))
  347. }
  348. // Handle security requirements if present and enabled
  349. if c.options.Authorization != "" {
  350. // Use custom authorization from options
  351. args = append(args, mcp.WithString("header|Authorization",
  352. mcp.Description("Authorization header"),
  353. mcp.DefaultString(c.options.Authorization)))
  354. } else if operation.Security != nil && len(*operation.Security) > 0 {
  355. securityArgs := c.convertSecurityRequirements(*operation.Security)
  356. args = append(args, securityArgs...)
  357. }
  358. // Create description that includes summary, description, and response information
  359. description := getDescription(operation)
  360. // Add response information to description
  361. // if operation.Responses != nil {
  362. // responseDesc := c.generateResponseDescription(*operation.Responses)
  363. // if responseDesc != "" {
  364. // description += "\n\nResponses:\n\n" + responseDesc
  365. // }
  366. // }
  367. args = append(args, mcp.WithDescription(description))
  368. tool := mcp.NewTool(toolName,
  369. args...,
  370. )
  371. return &tool
  372. }
  373. // generateResponseDescription creates a human-readable description of possible responses
  374. //
  375. //nolint:unused
  376. func (c *Converter) generateResponseDescription(responses openapi3.Responses) string {
  377. respMap := responses.Map()
  378. responseDescriptions := make([]string, 0, len(respMap))
  379. for code, responseRef := range respMap {
  380. if responseRef == nil || responseRef.Value == nil {
  381. continue
  382. }
  383. response := responseRef.Value
  384. desc := fmt.Sprintf("- status: %s, description: %s", code, *response.Description)
  385. rawSchema, ok := response.Extensions["schema"].(map[string]any)
  386. if ok && len(rawSchema) > 0 {
  387. jsonStr, err := sonic.Marshal(rawSchema)
  388. if err != nil {
  389. continue
  390. }
  391. schema := openapi3.Schema{}
  392. err = schema.UnmarshalJSON(jsonStr)
  393. if err != nil {
  394. continue
  395. }
  396. property := c.processSchemaProperty(&schema, make(map[string]bool))
  397. str, err := sonic.Marshal(property)
  398. if err != nil {
  399. continue
  400. }
  401. desc += fmt.Sprintf(", schema: %s", str)
  402. }
  403. if len(response.Content) > 0 {
  404. for contentType, mediaType := range response.Content {
  405. if mediaType.Schema != nil && mediaType.Schema.Value != nil {
  406. property := c.processSchemaProperty(
  407. mediaType.Schema.Value,
  408. make(map[string]bool),
  409. )
  410. str, err := sonic.Marshal(property)
  411. if err != nil {
  412. continue
  413. }
  414. desc += fmt.Sprintf(", content type: %s, schema: %s", contentType, str)
  415. }
  416. }
  417. }
  418. responseDescriptions = append(responseDescriptions, desc)
  419. }
  420. return strings.Join(responseDescriptions, "\n\n")
  421. }
  422. // convertSecurityRequirements converts OpenAPI security requirements to MCP arguments
  423. func (c *Converter) convertSecurityRequirements(
  424. securityRequirements openapi3.SecurityRequirements,
  425. ) []mcp.ToolOption {
  426. args := []mcp.ToolOption{}
  427. var securitySchemes openapi3.SecuritySchemes
  428. // Get security definitions from the document
  429. components := c.parser.GetDocument().Components
  430. if components != nil {
  431. securitySchemes = components.SecuritySchemes
  432. }
  433. // Process each security requirement
  434. for _, requirement := range securityRequirements {
  435. for schemeName, scopes := range requirement {
  436. var schemeRef *openapi3.SecuritySchemeRef
  437. if securitySchemes != nil {
  438. schemeRef = securitySchemes[schemeName]
  439. } else {
  440. args = append(args, mcp.WithString("header|Authorization",
  441. mcp.Description(fmt.Sprintf("API Key for %s authentication",
  442. schemeName)),
  443. mcp.Required()))
  444. }
  445. if schemeRef == nil || schemeRef.Value == nil {
  446. continue
  447. }
  448. scheme := schemeRef.Value
  449. switch scheme.Type {
  450. case "apiKey":
  451. args = append(args, mcp.WithString(scheme.In+"|"+scheme.Name,
  452. mcp.Description(fmt.Sprintf("API Key for %s authentication",
  453. schemeName)),
  454. mcp.Required()))
  455. case "http":
  456. switch scheme.Scheme {
  457. case "basic":
  458. args = append(args, mcp.WithString("openapi|auth_username",
  459. mcp.Description("Username for Basic authentication"),
  460. mcp.Required()))
  461. args = append(args, mcp.WithString("openapi|auth_password",
  462. mcp.Description("Password for Basic authentication"),
  463. mcp.Required()))
  464. case "bearer":
  465. args = append(args, mcp.WithString("openapi|auth_token",
  466. mcp.Description("Bearer token for authentication"),
  467. mcp.Required()))
  468. }
  469. case "oauth2":
  470. if len(scopes) > 0 {
  471. scopeDesc := "OAuth2 token with scopes: " + strings.Join(scopes, ", ")
  472. args = append(args, mcp.WithString("openapi|auth_oauth2_token",
  473. mcp.Description(scopeDesc),
  474. mcp.Required()))
  475. } else {
  476. args = append(args, mcp.WithString("openapi|auth_oauth2_token",
  477. mcp.Description("OAuth2 token for authentication"),
  478. mcp.Required()))
  479. }
  480. }
  481. }
  482. }
  483. return args
  484. }
  485. // convertRequestBody converts an OpenAPI request body to MCP arguments
  486. func (c *Converter) convertRequestBody(requestBody *openapi3.RequestBody) []mcp.ToolOption {
  487. args := []mcp.ToolOption{}
  488. for contentType, mediaType := range requestBody.Content {
  489. if mediaType.Schema == nil || mediaType.Schema.Value == nil {
  490. continue
  491. }
  492. schema := mediaType.Schema.Value
  493. propertyOptions := []mcp.PropertyOption{}
  494. if requestBody.Description != "" {
  495. propertyOptions = append(propertyOptions, mcp.Description(requestBody.Description))
  496. }
  497. if requestBody.Required {
  498. propertyOptions = append(propertyOptions, mcp.Required())
  499. }
  500. t := PropertyTypeObject
  501. if schema.Type != nil {
  502. switch {
  503. case schema.Type.Is("array") && schema.Items != nil && schema.Items.Value != nil:
  504. t = PropertyTypeArray
  505. item := c.processSchemaItems(schema.Items.Value, make(map[string]bool))
  506. propertyOptions = append(propertyOptions, mcp.Items(item))
  507. case schema.Type.Is("object") || len(schema.Properties) > 0:
  508. obj := c.processSchemaProperties(schema, make(map[string]bool))
  509. propertyOptions = append(propertyOptions, mcp.Properties(obj))
  510. case schema.Type.Is("string"):
  511. t = PropertyTypeString
  512. case schema.Type.Is("integer"):
  513. t = PropertyTypeInteger
  514. case schema.Type.Is("number"):
  515. t = PropertyTypeNumber
  516. case schema.Type.Is("boolean"):
  517. t = PropertyTypeBoolean
  518. }
  519. }
  520. // Add content type as part of the parameter name
  521. args = append(args, c.createToolOption(t, "body|"+contentType, propertyOptions...))
  522. }
  523. return args
  524. }
  525. type propertyType string
  526. const (
  527. PropertyTypeString propertyType = "string"
  528. PropertyTypeInteger propertyType = "integer"
  529. PropertyTypeNumber propertyType = "number"
  530. PropertyTypeBoolean propertyType = "boolean"
  531. PropertyTypeObject propertyType = "object"
  532. PropertyTypeArray propertyType = "array"
  533. )
  534. // convertParameters converts OpenAPI parameters to MCP arguments
  535. func (c *Converter) convertParameters(parameters openapi3.Parameters) []mcp.ToolOption {
  536. args := []mcp.ToolOption{}
  537. for _, paramRef := range parameters {
  538. param := paramRef.Value
  539. if param == nil {
  540. continue
  541. }
  542. propertyOptions := []mcp.PropertyOption{
  543. mcp.Description(param.Description),
  544. }
  545. if param.Required {
  546. propertyOptions = append(propertyOptions, mcp.Required())
  547. }
  548. t := PropertyTypeString
  549. if param.Schema != nil && param.Schema.Value != nil {
  550. schema := param.Schema.Value
  551. // Determine property type and add specific options
  552. switch {
  553. case schema.Type.Is("array") && schema.Items != nil && schema.Items.Value != nil:
  554. t = PropertyTypeArray
  555. item := c.processSchemaItems(schema.Items.Value, make(map[string]bool))
  556. propertyOptions = append(propertyOptions, mcp.Items(item))
  557. case schema.Type.Is("object") && len(schema.Properties) > 0:
  558. t = PropertyTypeObject
  559. obj := c.processSchemaProperties(schema, make(map[string]bool))
  560. propertyOptions = append(propertyOptions, mcp.Properties(obj))
  561. case schema.Type.Is("integer"):
  562. t = PropertyTypeInteger
  563. case schema.Type.Is("number"):
  564. t = PropertyTypeNumber
  565. case schema.Type.Is("boolean"):
  566. t = PropertyTypeBoolean
  567. }
  568. // Add enum values if present
  569. if len(schema.Enum) > 0 {
  570. enumValues := make([]string, 0, len(schema.Enum))
  571. for _, val := range schema.Enum {
  572. if strVal, ok := val.(string); ok {
  573. enumValues = append(enumValues, strVal)
  574. } else {
  575. // Convert non-string values to string
  576. enumValues = append(enumValues, fmt.Sprintf("%v", val))
  577. }
  578. }
  579. if len(enumValues) > 0 {
  580. propertyOptions = append(propertyOptions, mcp.Enum(enumValues...))
  581. }
  582. }
  583. // Add example if present
  584. if schema.Example != nil {
  585. propertyOptions = append(
  586. propertyOptions,
  587. mcp.DefaultString(fmt.Sprintf("%v", schema.Example)),
  588. )
  589. }
  590. }
  591. // Add the parameter based on its type
  592. if param.In == "body" {
  593. args = append(args, c.createToolOption(t, param.In, propertyOptions...))
  594. } else {
  595. args = append(args, c.createToolOption(t, param.In+"|"+param.Name, propertyOptions...))
  596. }
  597. }
  598. return args
  599. }
  600. // processSchemaItems processes schema items for array types
  601. func (c *Converter) processSchemaItems(
  602. schema *openapi3.Schema,
  603. visited map[string]bool,
  604. ) map[string]any {
  605. item := make(map[string]any)
  606. if schema.Type != nil {
  607. item["type"] = schema.Type
  608. }
  609. if schema.Description != "" {
  610. item["description"] = schema.Description
  611. }
  612. // Process nested properties if this is an object
  613. if len(schema.Properties) > 0 {
  614. properties := make(map[string]any)
  615. for propName, propRef := range schema.Properties {
  616. if propRef.Value != nil {
  617. properties[propName] = c.processSchemaProperty(propRef.Value, visited)
  618. }
  619. }
  620. item["properties"] = properties
  621. }
  622. // Handle reference if this is a reference to another schema
  623. if schema.Items != nil && schema.Items.Value != nil {
  624. item["items"] = c.processSchemaItems(schema.Items.Value, visited)
  625. }
  626. return item
  627. }
  628. // processSchemaProperties processes schema properties for object types
  629. func (c *Converter) processSchemaProperties(
  630. schema *openapi3.Schema,
  631. visited map[string]bool,
  632. ) map[string]any {
  633. obj := make(map[string]any)
  634. for propName, propRef := range schema.Properties {
  635. if propRef.Value != nil {
  636. obj[propName] = c.processSchemaProperty(propRef.Value, visited)
  637. }
  638. }
  639. return obj
  640. }
  641. // processSchemaProperty processes a single schema property
  642. func (c *Converter) processSchemaProperty(
  643. schema *openapi3.Schema,
  644. visited map[string]bool,
  645. ) map[string]any {
  646. // Check for circular references
  647. if schema.Title != "" {
  648. refKey := schema.Title
  649. if visited[refKey] {
  650. // We've seen this schema before, return a simplified reference to avoid circular
  651. // references
  652. return map[string]any{
  653. "type": "reference",
  654. "description": "Circular reference to " + refKey,
  655. "title": refKey,
  656. }
  657. }
  658. visited[refKey] = true
  659. // Create a copy of the visited map to avoid cross-contamination between different branches
  660. visitedCopy := make(map[string]bool)
  661. for k, v := range visited {
  662. visitedCopy[k] = v
  663. }
  664. visited = visitedCopy
  665. }
  666. return c.buildPropertyMap(schema, visited)
  667. }
  668. // buildPropertyMap builds the property map for a schema
  669. // This function was extracted to reduce cyclomatic complexity
  670. func (c *Converter) buildPropertyMap(
  671. schema *openapi3.Schema,
  672. visited map[string]bool,
  673. ) map[string]any {
  674. property := make(map[string]any)
  675. // Add basic schema information
  676. c.addBasicSchemaInfo(schema, property)
  677. // Add schema validations
  678. c.addSchemaValidations(schema, property)
  679. // Add schema composition
  680. c.addSchemaComposition(schema, property, visited)
  681. // Add object properties
  682. c.addObjectProperties(schema, property, visited)
  683. // Add array items
  684. c.addArrayItems(schema, property, visited)
  685. // Add additional schema metadata
  686. c.addAdditionalMetadata(schema, property)
  687. return property
  688. }
  689. // addBasicSchemaInfo adds basic schema information to the property map
  690. func (c *Converter) addBasicSchemaInfo(schema *openapi3.Schema, property map[string]any) {
  691. if schema.Type != nil {
  692. property["type"] = schema.Type
  693. }
  694. // Basic metadata
  695. if schema.Title != "" {
  696. property["title"] = schema.Title
  697. }
  698. if schema.Description != "" {
  699. property["description"] = schema.Description
  700. }
  701. if schema.Default != nil {
  702. property["default"] = schema.Default
  703. }
  704. if schema.Example != nil {
  705. property["example"] = schema.Example
  706. }
  707. if len(schema.Enum) > 0 {
  708. property["enum"] = schema.Enum
  709. }
  710. if schema.Format != "" {
  711. property["format"] = schema.Format
  712. }
  713. }
  714. // addSchemaValidations adds schema validations to the property map
  715. func (c *Converter) addSchemaValidations(schema *openapi3.Schema, property map[string]any) {
  716. // Boolean flags
  717. if schema.Nullable {
  718. property["nullable"] = schema.Nullable
  719. }
  720. if schema.ReadOnly {
  721. property["readOnly"] = schema.ReadOnly
  722. }
  723. if schema.WriteOnly {
  724. property["writeOnly"] = schema.WriteOnly
  725. }
  726. if schema.Deprecated {
  727. property["deprecated"] = schema.Deprecated
  728. }
  729. if schema.AllowEmptyValue {
  730. property["allowEmptyValue"] = schema.AllowEmptyValue
  731. }
  732. if schema.UniqueItems {
  733. property["uniqueItems"] = schema.UniqueItems
  734. }
  735. if schema.ExclusiveMin {
  736. property["exclusiveMinimum"] = schema.ExclusiveMin
  737. }
  738. if schema.ExclusiveMax {
  739. property["exclusiveMaximum"] = schema.ExclusiveMax
  740. }
  741. // Number validations
  742. if schema.Min != nil {
  743. property["minimum"] = *schema.Min
  744. }
  745. if schema.Max != nil {
  746. property["maximum"] = *schema.Max
  747. }
  748. if schema.MultipleOf != nil {
  749. property["multipleOf"] = *schema.MultipleOf
  750. }
  751. // String validations
  752. if schema.MinLength != 0 {
  753. property["minLength"] = schema.MinLength
  754. }
  755. if schema.MaxLength != nil {
  756. property["maxLength"] = *schema.MaxLength
  757. }
  758. if schema.Pattern != "" {
  759. property["pattern"] = schema.Pattern
  760. }
  761. // Array validations
  762. if schema.MinItems != 0 {
  763. property["minItems"] = schema.MinItems
  764. }
  765. if schema.MaxItems != nil {
  766. property["maxItems"] = *schema.MaxItems
  767. }
  768. // Object validations
  769. if schema.MinProps != 0 {
  770. property["minProperties"] = schema.MinProps
  771. }
  772. if schema.MaxProps != nil {
  773. property["maxProperties"] = *schema.MaxProps
  774. }
  775. if len(schema.Required) > 0 {
  776. property["required"] = schema.Required
  777. }
  778. }
  779. // addSchemaComposition adds schema composition to the property map
  780. func (c *Converter) addSchemaComposition(
  781. schema *openapi3.Schema,
  782. property map[string]any,
  783. visited map[string]bool,
  784. ) {
  785. // Schema composition
  786. if len(schema.OneOf) > 0 {
  787. oneOf := make([]any, 0, len(schema.OneOf))
  788. for _, schemaRef := range schema.OneOf {
  789. if schemaRef.Value != nil {
  790. oneOf = append(oneOf, c.processSchemaProperty(schemaRef.Value, visited))
  791. }
  792. }
  793. if len(oneOf) > 0 {
  794. property["oneOf"] = oneOf
  795. }
  796. }
  797. if len(schema.AnyOf) > 0 {
  798. anyOf := make([]any, 0, len(schema.AnyOf))
  799. for _, schemaRef := range schema.AnyOf {
  800. if schemaRef.Value != nil {
  801. anyOf = append(anyOf, c.processSchemaProperty(schemaRef.Value, visited))
  802. }
  803. }
  804. if len(anyOf) > 0 {
  805. property["anyOf"] = anyOf
  806. }
  807. }
  808. if len(schema.AllOf) > 0 {
  809. allOf := make([]any, 0, len(schema.AllOf))
  810. for _, schemaRef := range schema.AllOf {
  811. if schemaRef.Value != nil {
  812. allOf = append(allOf, c.processSchemaProperty(schemaRef.Value, visited))
  813. }
  814. }
  815. if len(allOf) > 0 {
  816. property["allOf"] = allOf
  817. }
  818. }
  819. if schema.Not != nil && schema.Not.Value != nil {
  820. property["not"] = c.processSchemaProperty(schema.Not.Value, visited)
  821. }
  822. }
  823. // addObjectProperties adds object properties to the property map
  824. func (c *Converter) addObjectProperties(
  825. schema *openapi3.Schema,
  826. property map[string]any,
  827. visited map[string]bool,
  828. ) {
  829. // Handle AdditionalProperties
  830. if schema.AdditionalProperties.Has != nil {
  831. property["additionalProperties"] = *schema.AdditionalProperties.Has
  832. } else if schema.AdditionalProperties.Schema != nil && schema.AdditionalProperties.Schema.Value != nil {
  833. property["additionalProperties"] = c.processSchemaProperty(schema.AdditionalProperties.Schema.Value, visited)
  834. }
  835. // Handle discriminator
  836. if schema.Discriminator != nil {
  837. discriminator := make(map[string]any)
  838. discriminator["propertyName"] = schema.Discriminator.PropertyName
  839. if len(schema.Discriminator.Mapping) > 0 {
  840. discriminator["mapping"] = schema.Discriminator.Mapping
  841. }
  842. property["discriminator"] = discriminator
  843. }
  844. // Recursively process nested objects
  845. if schema.Type != nil && schema.Type.Is("object") && len(schema.Properties) > 0 {
  846. nestedProps := make(map[string]any)
  847. for propName, propRef := range schema.Properties {
  848. if propRef.Value != nil {
  849. nestedProps[propName] = c.processSchemaProperty(propRef.Value, visited)
  850. }
  851. }
  852. property["properties"] = nestedProps
  853. }
  854. }
  855. func (c *Converter) addArrayItems(
  856. schema *openapi3.Schema,
  857. property map[string]any,
  858. visited map[string]bool,
  859. ) {
  860. // Recursively process array items
  861. if schema.Type != nil && schema.Type.Is("array") && schema.Items != nil &&
  862. schema.Items.Value != nil {
  863. property["items"] = c.processSchemaItems(schema.Items.Value, visited)
  864. }
  865. }
  866. func (c *Converter) addAdditionalMetadata(schema *openapi3.Schema, property map[string]any) {
  867. // Handle external docs if present
  868. if schema.ExternalDocs != nil {
  869. externalDocs := make(map[string]any)
  870. if schema.ExternalDocs.Description != "" {
  871. externalDocs["description"] = schema.ExternalDocs.Description
  872. }
  873. if schema.ExternalDocs.URL != "" {
  874. externalDocs["url"] = schema.ExternalDocs.URL
  875. }
  876. property["externalDocs"] = externalDocs
  877. }
  878. // Handle XML object if present
  879. if schema.XML != nil {
  880. xml := make(map[string]any)
  881. if schema.XML.Name != "" {
  882. xml["name"] = schema.XML.Name
  883. }
  884. if schema.XML.Namespace != "" {
  885. xml["namespace"] = schema.XML.Namespace
  886. }
  887. if schema.XML.Prefix != "" {
  888. xml["prefix"] = schema.XML.Prefix
  889. }
  890. xml["attribute"] = schema.XML.Attribute
  891. xml["wrapped"] = schema.XML.Wrapped
  892. property["xml"] = xml
  893. }
  894. }
  895. // createToolOption creates the appropriate tool option based on property type
  896. func (c *Converter) createToolOption(
  897. t propertyType,
  898. name string,
  899. options ...mcp.PropertyOption,
  900. ) mcp.ToolOption {
  901. switch t {
  902. case PropertyTypeString:
  903. return mcp.WithString(name, options...)
  904. case PropertyTypeInteger:
  905. return mcp.WithNumber(name, options...)
  906. case PropertyTypeNumber:
  907. return mcp.WithNumber(name, options...)
  908. case PropertyTypeBoolean:
  909. return mcp.WithBoolean(name, options...)
  910. case PropertyTypeObject:
  911. return mcp.WithObject(name, options...)
  912. case PropertyTypeArray:
  913. return mcp.WithArray(name, options...)
  914. default:
  915. return mcp.WithString(name, options...)
  916. }
  917. }
  918. // getDescription returns a description for an operation
  919. func getDescription(operation *openapi3.Operation) string {
  920. var parts []string
  921. if operation.Summary != "" {
  922. parts = append(parts, operation.Summary)
  923. }
  924. if operation.Description != "" {
  925. parts = append(parts, operation.Description)
  926. }
  927. // Add deprecated notice if applicable
  928. if operation.Deprecated {
  929. parts = append(parts, "WARNING: This operation is deprecated.")
  930. }
  931. return strings.Join(parts, "\n\n")
  932. }