Skip to content

achetronic/openapi2tools

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Openapi2Tools

Openapi2Tools is a small Go library that turns OpenAPI operations into Model Context Protocol (MCP) tools at runtime

Features

  • Load OpenAPI 3.x specs from a URL, a file, or raw bytes (YAML or JSON).
  • Inline $refs, strip examples, and make integer/boolean parameters tolerant of string values (handy when LLMs serialise 5 as "5").
  • Filter the routes you expose with regex and HTTP method rules.
  • Generate snake*case tool names from operationId, or fall back to <method>*<path>.
  • Build a JSON Schema input from path, query, header and body parameters, preserving nested fields, enums, formats, patterns, minItems, oneOf, etc.
  • HTTP executor that substitutes path params, encodes query params (including repeated array values), forwards headers and sends the rest as JSON.
  • Per-tool description overrides and response formatters when you need to tweak the output.

Install

go get github.com/achetronic/openapi2tools

Quick start

Pick the SDK your project uses and expand the section below. The only difference between the two is the adapter import and the server type at the end.

Using mark3labs/mcp-go
package main

import (
    "log"

    "github.com/mark3labs/mcp-go/server"

    "github.com/achetronic/openapi2tools/adapters/mcpgo"
    "github.com/achetronic/openapi2tools/mcptools"
    "github.com/achetronic/openapi2tools/openapi"
)

func main() {
    // 1. Load the OpenAPI spec.
    loader := openapi.NewLoader(openapi.LoadOptions{
        ResolveRefs:        true,
        RemoveExamples:     true,
        FlexibleParameters: true,
    })
    spec, err := loader.LoadURL("https://example.com/openapi.yaml")
    if err != nil {
        log.Fatal(err)
    }

    // 2. Pick which routes you want to expose.
    filters, _ := openapi.CompileRouteFilters([]openapi.RouteFilterConfig{
        {Methods: []string{"GET", "POST"}, Paths: `^/v1/.*`},
    })
    routes := openapi.FilterRoutes(spec, filters)

    // 3. Configure an HTTP executor that calls the underlying API.
    exec := &mcptools.HTTPExecutor{
        BaseURL: "https://example.com",
        DefaultHeaders: map[string]string{
            "User-Agent": "my-mcp/1.0",
        },
    }

    // 4. Describe the routes as ToolDescriptors.
    tools := mcptools.Describe(routes, mcptools.DescribeOptions{
        CustomHandlerFactory: mcptools.HTTPHandlerFactory(exec),
    })

    // 5. Register them on a mark3labs/mcp-go server.
    mcp := server.NewMCPServer("my-mcp", "1.0.0")
    if _, err := mcpgo.Register(mcp, tools); err != nil {
        log.Fatal(err)
    }

    log.Fatal(server.ServeStdio(mcp))
}
Using modelcontextprotocol/go-sdk (official)
package main

import (
    "context"
    "log"

    "github.com/modelcontextprotocol/go-sdk/mcp"

    "github.com/achetronic/openapi2tools/adapters/mcpsdk"
    "github.com/achetronic/openapi2tools/mcptools"
    "github.com/achetronic/openapi2tools/openapi"
)

func main() {
    loader := openapi.NewLoader(openapi.LoadOptions{ResolveRefs: true})
    spec, err := loader.LoadURL("https://example.com/openapi.yaml")
    if err != nil {
        log.Fatal(err)
    }

    routes := openapi.FilterRoutes(spec, nil) // nil = include all routes

    exec := &mcptools.HTTPExecutor{BaseURL: "https://example.com"}
    tools := mcptools.Describe(routes, mcptools.DescribeOptions{
        CustomHandlerFactory: mcptools.HTTPHandlerFactory(exec),
    })

    server := mcp.NewServer(&mcp.Implementation{
        Name:    "my-mcp",
        Version: "1.0.0",
    }, nil)

    if _, err := mcpsdk.Register(server, tools); err != nil {
        log.Fatal(err)
    }

    if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
        log.Fatal(err)
    }
}

Loading specs: LoadOptions

openapi.NewLoader takes a LoadOptions struct that controls how the spec is fetched, parsed, transformed and cached. Every field is optional; the zero value gives you a plain parse with no transformations.

type LoadOptions struct {
    ResolveRefs        bool
    RemoveExamples     bool
    FlexibleParameters bool

    HTTPClient    *http.Client
    CachePath     string
    CacheDuration time.Duration
}
Field Type Default What it does
ResolveRefs bool false Inline every internal $ref so each operation has a self-contained schema.
RemoveExamples bool false Strip example / examples keys to shrink the schemas you give an LLM.
FlexibleParameters bool false Accept "5" / "true" in addition to 5 / true for numeric/bool args.
HTTPClient *http.Client &http.Client{Timeout: 30s} HTTP client used by LoadURL (proxies, custom transports, retries…).
CachePath string "" (disabled) On-disk cache for the post-processed spec when loading from a URL.
CacheDuration time.Duration 1 * time.Hour TTL for the on-disk cache. Ignored when CachePath is empty.

A few cross-cutting notes:

  • ResolveRefs runs before RemoveExamples and FlexibleParameters, so both transformations also apply to the inlined material.
  • The cache stores the transformed YAML. If you change the options, bump CachePath or delete the file so the next load regenerates it.
  • LoadFile and LoadBytes ignore HTTPClient, CachePath and CacheDuration; the rest of the options still apply.
  • $ref cycles are guarded by an internal max-depth of 20.

Typical recipes

// 1) Just load a spec, faithfully:
loader := openapi.NewLoader(openapi.LoadOptions{})

// 2) Recommended when feeding tools to an LLM:
loader := openapi.NewLoader(openapi.LoadOptions{
    ResolveRefs:        true,  // self-contained schemas
    RemoveExamples:     true,  // smaller payloads
    FlexibleParameters: true,  // resilient to "5" vs 5
})

// 3) Same as above, but cache the processed spec on disk:
loader := openapi.NewLoader(openapi.LoadOptions{
    ResolveRefs:    true,
    RemoveExamples: true,
    CachePath:      filepath.Join(os.TempDir(), "myapi-"+openapi.CacheKey(specURL)+".yaml"),
    CacheDuration:  6 * time.Hour,
})

Filtering routes: FilterRoutes

After loading a spec you usually want to expose only a subset of its operations. openapi.FilterRoutes is a regex-based filter that turns a *Spec into a list of RouteMatch (one per path + HTTP method to expose).

func FilterRoutes(spec *Spec, filters []RouteFilter) []RouteMatch
  • spec: the parsed spec returned by the loader.
  • filters: an ordered list of rules. Pass nil (or an empty slice) to include every operation in the spec.

A RouteFilter (or its YAML/JSON-friendly twin RouteFilterConfig) has three fields:

type RouteFilter struct {
    Methods []string        // HTTP methods this rule applies to ([] = all)
    Paths   *regexp.Regexp  // regex matched against the route path
    Exclude bool            // true = drop matching routes
}

Evaluation model

For every (path, method) combination in the spec, filters are evaluated in order:

  1. The first filter whose Paths matches the path and whose Methods contains the method (or whose Methods is empty) wins.
  2. If that filter has Exclude: false, the route is kept. If Exclude: true, it is dropped.
  3. Routes that match no filter are dropped.

That last point is important: filters work as an allow-list by default. If you want a deny-list style ("everything except…"), add a final catch-all rule with Paths: .*.

From config files

If your filters come from YAML or JSON, use CompileRouteFilters to turn them into regex-backed RouteFilters:

type RouteFilterConfig struct {
    Methods []string `yaml:"methods,omitempty"`
    Paths   string   `yaml:"paths"`
    Exclude bool     `yaml:"exclude,omitempty"`
}

Recipes

Expose everything in the spec:

routes := openapi.FilterRoutes(spec, nil)

Expose only read-only endpoints under /v1/:

filters, _ := openapi.CompileRouteFilters([]openapi.RouteFilterConfig{
    {Methods: []string{"GET"}, Paths: `^/v1/.*`},
})
routes := openapi.FilterRoutes(spec, filters)

Expose everything except DELETE operations:

filters, _ := openapi.CompileRouteFilters([]openapi.RouteFilterConfig{
    {Methods: []string{"DELETE"}, Paths: `.*`, Exclude: true},
    {Paths: `.*`},
})
routes := openapi.FilterRoutes(spec, filters)

Pin a curated subset (order matters: more specific rules first):

filters, _ := openapi.CompileRouteFilters([]openapi.RouteFilterConfig{
    {Methods: []string{"POST"},        Paths: `^/v1/jobs$`},
    {Methods: []string{"GET"},         Paths: `^/v1/jobs/[^/]+$`},
    {Methods: []string{"GET", "POST"}, Paths: `^/v1/catalog/.*`},
    // Anything that didn't match above is implicitly dropped.
})

Architecture

flowchart TD
    A[openapi.Loader]
    B[openapi.FilterRoutes]
    C[mcptools.Describe]
    D[adapters/mcpgo]
    E[adapters/mcpsdk]

    A -- "*openapi.Spec" --> B
    B -- "[]RouteMatch" --> C
    C -- "[]ToolDescriptor" --> D
    C -- "[]ToolDescriptor" --> E

    classDef core fill:#1f6feb,stroke:#0b3d91,color:#ffffff;
    classDef adapter fill:#2ea44f,stroke:#1a7f37,color:#ffffff;
    class A,B,C core;
    class D,E adapter;
Loading
  • openapi.Loader reads the spec from a URL, a file or raw bytes.
  • openapi.FilterRoutes keeps only the routes you want, using regex and HTTP methods.
  • mcptools.Describe turns each route into a ToolDescriptor (name, description, JSON Schema, handler).
  • adapters/mcpgo registers those descriptors on a mark3labs/mcp-go server.
  • adapters/mcpsdk does the same on a modelcontextprotocol/go-sdk server.

Each layer is independent, so you can:

  • Drop the HTTPExecutor and provide your own handler factory.
  • Use only the schema package to build JSON Schemas from openapi.Schema values.
  • Write your own adapter for any other MCP library. A ToolDescriptor is just a name, a description, a JSON Schema map, an OpenAPI route and a handler function.

Packages

Package What it does
openapi OpenAPI 3.x types, loader, $ref resolver and route filters.
schema Convert openapi.Schema to map[string]any (JSON Schema) and build the full input schema for an operation.
mcptools Library-agnostic ToolDescriptor, DescribeOptions and HTTPExecutor.
adapters/mcpgo Register tools on a mark3labs/mcp-go server.
adapters/mcpsdk Register tools on a modelcontextprotocol/go-sdk server.

Examples

See examples/ for runnable programs:

Status

Extracted from a production MCP. The core types and the schema converter have been used against real OpenAPI specs.

License

Apache 2.0

About

Library to turn OpenAPI operations into Model Context Protocol (MCP) tools at runtime

Topics

Resources

License

Stars

2 stars

Watchers

0 watching

Forks

Contributors

Languages