convert.go 28 KB

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