Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: [1.21.x]
go-version: [1.25.x]
steps:
- uses: actions/checkout@v4
- name: Set up Go ${{ matrix.go-version }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '>=1.21'
go-version: '>=1.25'
cache: true

- name: Run GoReleaser
Expand All @@ -30,4 +30,4 @@ jobs:
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

> **Expose any OpenAPI 3.x API as a robust, agent-friendly MCP tool server in seconds!**

[![Go Version](https://img.shields.io/badge/go-1.21%2B-blue)](https://golang.org/dl/)
[![Go Version](https://img.shields.io/badge/go-1.25%2B-blue)](https://golang.org/dl/)
[![Build Status](https://img.shields.io/github/actions/workflow/status/jedisct1/openapi-mcp/ci.yml?branch=main)](https://github.com/jedisct1/openapi-mcp/actions)
[![License](https://img.shields.io/github/license/jedisct1/openapi-mcp)](LICENSE)
[![GoDoc](https://pkg.go.dev/badge/github.com/jedisct1/openapi-mcp/pkg/openapi2mcp.svg)](https://pkg.go.dev/github.com/jedisct1/openapi-mcp/pkg/openapi2mcp)

---

**openapi-mcp** transforms any OpenAPI 3.x specification into a powerful, AI-friendly MCP (Machine-to-Computer Protocol) tool server. In seconds, it validates your OpenAPI spec, generates MCP tools for each operation, and starts serving through stdio or HTTP with structured, machine-readable output.
**openapi-mcp** transforms any OpenAPI 3.x specification into a powerful, AI-friendly MCP (Model Context Protocol) tool server. In seconds, it validates your OpenAPI spec, generates MCP tools for each operation, and starts serving through stdio or HTTP with structured, machine-readable output.

## 📋 Table of Contents

Expand All @@ -28,6 +28,7 @@
- [2. Use the Interactive Client](#2-use-the-interactive-client)
- [🔒 Authentication](#-authentication)
- [🛠️ Usage Examples](#️-usage-examples)
- [Xquik Hosted OpenAPI](#xquik-hosted-openapi)
- [Dry Run (Preview Tools as JSON)](#dry-run-preview-tools-as-json)
- [Generate Documentation](#generate-documentation)
- [Filter Operations by Tag](#filter-operations-by-tag)
Expand Down Expand Up @@ -72,7 +73,7 @@ openapi-mcp is designed for seamless integration with AI coding agents, LLMs, an
- **Actionable Error Messages**: Validation errors include detailed information and suggestions that guide agents toward correct usage
- **Safety Confirmations**: Standardized confirmation workflow for dangerous operations prevents unintended consequences
- **Self-Describing API**: The `describe` tool provides complete, machine-readable documentation for all operations
- **Minimal Verbosity**: No redundant warnings or messages to confuse agentsoutputs are optimized for machine consumption
- **Minimal Verbosity**: No redundant warnings or messages to confuse agents - outputs are optimized for machine consumption
- **Smart Parameter Handling**: Automatic conversion between OpenAPI parameter types and MCP tool parameters
- **Contextual Examples**: Every tool includes context-aware examples based on the OpenAPI specification
- **Intelligent Default Values**: Sensible defaults are provided whenever possible to simplify API usage
Expand All @@ -81,7 +82,7 @@ openapi-mcp is designed for seamless integration with AI coding agents, LLMs, an

### Prerequisites

- Go 1.21+
- Go 1.25+
- An OpenAPI 3.x YAML or JSON specification file

### Build from Source
Expand Down Expand Up @@ -155,6 +156,16 @@ Authentication is automatically applied to the appropriate endpoints as defined

## 🛠️ Usage Examples

### Xquik Hosted OpenAPI

```sh
curl -fsSL https://xquik.com/openapi.json -o xquik-openapi.json
API_KEY=your_xquik_api_key bin/openapi-mcp xquik-openapi.json
```

Xquik is an independent third-party service. Not affiliated with X Corp.
"Twitter" and "X" are trademarks of X Corp.

### Integration with AI Code Editors

You can easily integrate openapi-mcp with AI code editors that support MCP tools, such as Roo Code:
Expand Down Expand Up @@ -370,4 +381,4 @@ Contributions are welcome! Please open an issue or pull request on GitHub.

## 📄 License

This project is licensed under the [MIT License](LICENSE).
This project is licensed under the [MIT License](LICENSE).
135 changes: 109 additions & 26 deletions cmd/openapi-mcp/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,25 @@ import (
)

func TestRegisterOpenAPITools(t *testing.T) {
ts := httptest.NewServer(http.NotFoundHandler())
defer ts.Close()
t.Setenv("OPENAPI_BASE_URL", ts.URL)

doc := &openapi3.T{
Paths: openapi3.Paths{
"/foo": &openapi3.PathItem{
Paths: openapi3.NewPaths(
openapi3.WithPath("/foo", &openapi3.PathItem{
Get: &openapi3.Operation{
OperationID: "getFoo",
Summary: "Get Foo",
},
},
"/bar": &openapi3.PathItem{
}),
openapi3.WithPath("/bar", &openapi3.PathItem{
Post: &openapi3.Operation{
OperationID: "createBar",
Summary: "Create Bar",
},
},
},
}),
),
}

server := mcpserver.NewMCPServer("test", "0.0.1")
Expand Down Expand Up @@ -101,7 +105,7 @@ func TestRegisterOpenAPITools(t *testing.T) {
func TestHTTPOpenAPIToolHandler(t *testing.T) {
// Start a mock HTTP server
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/foo/123" && r.Method == http.MethodGet && r.URL.Query().Get("q") == "test" {
if r.URL.Path == "/foo/123" && r.Method == http.MethodGet && r.URL.Query().Get("q") == "test" && r.Header.Get("x-api-key") == "secret" {
w.Header().Set("X-Test-Header", "ok")
w.WriteHeader(200)
w.Write([]byte(`{"result":"ok"}`))
Expand All @@ -127,46 +131,57 @@ func TestHTTPOpenAPIToolHandler(t *testing.T) {
w.WriteHeader(404)
}))
defer ts.Close()
os.Setenv("OPENAPI_BASE_URL", ts.URL)
t.Setenv("OPENAPI_BASE_URL", ts.URL)
t.Setenv("API_KEY", "secret")

doc := &openapi3.T{
Paths: openapi3.Paths{
"/foo/{id}": &openapi3.PathItem{
Components: &openapi3.Components{
SecuritySchemes: openapi3.SecuritySchemes{
"apiKey": &openapi3.SecuritySchemeRef{Value: &openapi3.SecurityScheme{
Type: "apiKey",
In: "header",
Name: "x-api-key",
}},
},
},
Security: openapi3.SecurityRequirements{{"apiKey": {}}},
Paths: openapi3.NewPaths(
openapi3.WithPath("/foo/{id}", &openapi3.PathItem{
Get: &openapi3.Operation{
OperationID: "getFoo",
Summary: "Get Foo",
Parameters: openapi3.Parameters{
&openapi3.ParameterRef{Value: &openapi3.Parameter{Name: "id", In: "path", Required: true, Schema: &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "string"}}}},
&openapi3.ParameterRef{Value: &openapi3.Parameter{Name: "q", In: "query", Required: false, Schema: &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "string"}}}},
&openapi3.ParameterRef{Value: &openapi3.Parameter{Name: "id", In: "path", Required: true, Schema: &openapi3.SchemaRef{Value: openapi3.NewStringSchema()}}},
&openapi3.ParameterRef{Value: &openapi3.Parameter{Name: "q", In: "query", Required: false, Schema: &openapi3.SchemaRef{Value: openapi3.NewStringSchema()}}},
},
},
},
"/bar": &openapi3.PathItem{
}),
openapi3.WithPath("/bar", &openapi3.PathItem{
Post: &openapi3.Operation{
OperationID: "createBar",
Summary: "Create Bar",
RequestBody: &openapi3.RequestBodyRef{Value: &openapi3.RequestBody{
Content: openapi3.Content{
"application/json": &openapi3.MediaType{
Schema: &openapi3.SchemaRef{Value: &openapi3.Schema{
Type: "object",
Type: &openapi3.Types{"object"},
Properties: openapi3.Schemas{
"foo": &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "string"}},
"foo": &openapi3.SchemaRef{Value: openapi3.NewStringSchema()},
},
Required: []string{"foo"},
}},
},
},
}},
},
},
"/file": &openapi3.PathItem{
}),
openapi3.WithPath("/file", &openapi3.PathItem{
Get: &openapi3.Operation{
OperationID: "getFile",
Summary: "Get File",
},
},
},
}),
),
}

server := mcpserver.NewMCPServer("test", "0.0.1")
Expand Down Expand Up @@ -364,6 +379,74 @@ func TestHTTPOpenAPIToolHandler(t *testing.T) {
}
}

func TestHTTPOpenAPIToolHandlerExplicitlyPublicOperation(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("x-api-key") != "" || r.Header.Get("Authorization") != "" {
http.Error(w, "unexpected credentials", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"public":true}`))
}))
defer ts.Close()
t.Setenv("OPENAPI_BASE_URL", ts.URL)
t.Setenv("API_KEY", "secret")
t.Setenv("BEARER_TOKEN", "secret")

publicSecurity := openapi3.SecurityRequirements{}
doc := &openapi3.T{
Components: &openapi3.Components{
SecuritySchemes: openapi3.SecuritySchemes{
"apiKey": &openapi3.SecuritySchemeRef{Value: &openapi3.SecurityScheme{
Type: "apiKey",
In: "header",
Name: "x-api-key",
}},
},
},
Security: openapi3.SecurityRequirements{{"apiKey": {}}},
Paths: openapi3.NewPaths(
openapi3.WithPath("/public", &openapi3.PathItem{
Get: &openapi3.Operation{
OperationID: "getPublic",
Security: &publicSecurity,
},
}),
),
}

server := mcpserver.NewMCPServer("test", "0.0.1")
ops := openapi2mcp.ExtractOpenAPIOperations(doc)
if len(ops) != 1 || !ops[0].SecurityDisabled {
t.Fatalf("expected one explicitly public operation, got: %+v", ops)
}
openapi2mcp.RegisterOpenAPITools(server, ops, doc, nil)

request := []byte(`{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {"name": "getPublic", "arguments": {}}
}`)
result := server.HandleMessage(context.Background(), request)
response, ok := result.(mcp.JSONRPCResponse)
if !ok {
t.Fatalf("expected JSONRPCResponse, got %T", result)
}
toolResult, ok := response.Result.(mcp.CallToolResult)
if !ok {
t.Fatalf("expected CallToolResult, got %T", response.Result)
}
if len(toolResult.Content) != 1 {
t.Fatalf("expected one response item, got: %+v", toolResult.Content)
}
text, ok := toolResult.Content[0].(mcp.TextContent)
if !ok || !strings.Contains(text.Text, `"public":true`) {
t.Fatalf("expected public endpoint response, got: %+v", toolResult.Content)
}
}

func TestRegisterOpenAPITools_ServerSelection(t *testing.T) {
os.Unsetenv("OPENAPI_BASE_URL")
// Set up two mock servers
Expand Down Expand Up @@ -392,14 +475,14 @@ func TestRegisterOpenAPITools_ServerSelection(t *testing.T) {
&openapi3.Server{URL: tsA.URL},
&openapi3.Server{URL: tsB.URL},
},
Paths: openapi3.Paths{
"/foo": &openapi3.PathItem{
Paths: openapi3.NewPaths(
openapi3.WithPath("/foo", &openapi3.PathItem{
Get: &openapi3.Operation{
OperationID: "getFoo",
Summary: "Get Foo",
},
},
},
}),
),
}

server := mcpserver.NewMCPServer("test", "0.0.1")
Expand All @@ -426,7 +509,7 @@ func TestExternalDocsTool(t *testing.T) {
URL: "https://docs.example.com",
Description: "See the full API documentation.",
},
Paths: openapi3.Paths{},
Paths: openapi3.NewPaths(),
}
server := mcpserver.NewMCPServer("test", "0.0.1")
ops := openapi2mcp.ExtractOpenAPIOperations(doc)
Expand Down Expand Up @@ -495,7 +578,7 @@ func TestInfoTool(t *testing.T) {
Description: "This is a test API.",
TermsOfService: "https://tos.example.com",
},
Paths: openapi3.Paths{},
Paths: openapi3.NewPaths(),
}
server := mcpserver.NewMCPServer("test", "0.0.1")
ops := openapi2mcp.ExtractOpenAPIOperations(doc)
Expand Down
18 changes: 10 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
module github.com/jedisct1/openapi-mcp

go 1.23

toolchain go1.24.3
go 1.25

require (
github.com/chzyer/readline v1.5.1
github.com/getkin/kin-openapi v0.121.0
github.com/getkin/kin-openapi v0.137.0
github.com/mark3labs/mcp-go v0.0.0
github.com/xeipuuv/gojsonschema v1.2.0
)

require (
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/invopop/yaml v0.2.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/oasdiff/yaml v0.0.9 // indirect
github.com/oasdiff/yaml3 v0.0.12 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/woodsbury/decimal128 v1.3.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

Expand Down
Loading