├── .github ├── banner.png └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yaml ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── mcp-client │ └── main.go └── openapi-mcp │ ├── doc.go │ ├── flags.go │ ├── main.go │ ├── main_test.go │ ├── server.go │ └── utils.go ├── docs ├── css │ └── styles.css ├── docs │ ├── advanced.html │ ├── ai-integration.html │ ├── authentication.html │ ├── cli.md │ ├── command-line.html │ ├── configuration.html │ ├── index.html │ ├── installation.html │ ├── library-usage.html │ ├── output-structure.html │ ├── quick-start.html │ └── safety-features.html ├── examples │ └── index.html ├── images │ ├── favicon.ico │ └── logo.svg ├── index.html └── js │ └── main.js ├── examples └── fastly-openapi-mcp.yaml ├── go.mod ├── go.sum ├── openapi-validator ├── README.md ├── index.html ├── script.js └── styles.css └── pkg ├── mcp ├── .golangci.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── mcp │ ├── prompts.go │ ├── resources.go │ ├── tools.go │ ├── types.go │ └── utils.go ├── server │ ├── errors.go │ ├── hooks.go │ ├── http_transport_options.go │ ├── internal │ │ └── gen │ │ │ ├── README.md │ │ │ ├── data.go │ │ │ ├── hooks.go.tmpl │ │ │ ├── main.go │ │ │ └── request_handler.go.tmpl │ ├── request_handler.go │ ├── server.go │ ├── server_test.go │ ├── session.go │ ├── sse.go │ ├── stdio.go │ ├── streamable_http.go │ └── streamable_http_test.go └── util │ └── logger.go └── openapi2mcp ├── README.md ├── http_lint.go ├── openapi2mcp.go ├── register.go ├── register_test.go ├── schema.go ├── schema_test.go ├── selftest.go ├── selftest_test.go ├── server.go ├── server_test.go ├── spec.go ├── summary.go └── types.go /.github/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jedisct1/openapi-mcp/008c33e35ee1e8f025911da4b1ae50a22f8161dc/.github/banner.png -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Go CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | go-version: [1.21.x] 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Set up Go ${{ matrix.go-version }} 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: ${{ matrix.go-version }} 21 | - name: Cache Go modules 22 | uses: actions/cache@v4 23 | with: 24 | path: | 25 | ~/.cache/go-build 26 | ~/go/pkg/mod 27 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 28 | restore-keys: | 29 | ${{ runner.os }}-go- 30 | - name: Install dependencies 31 | run: go mod tidy 32 | - name: Build 33 | run: make 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Set up Go 21 | uses: actions/setup-go@v4 22 | with: 23 | go-version: '>=1.21' 24 | cache: true 25 | 26 | - name: Run GoReleaser 27 | uses: goreleaser/goreleaser-action@v4 28 | with: 29 | distribution: goreleaser 30 | version: latest 31 | args: release --clean 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .claude 2 | .zig-cache 3 | *~ 4 | bin/ 5 | AUTH.md 6 | AI-OPTS.md 7 | CLAUDE.md 8 | RELEASE.md 9 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # GoReleaser configuration for openapi-mcp 2 | # This will build and release binaries for multiple platforms when a new tag is pushed 3 | 4 | version: 2 5 | 6 | before: 7 | hooks: 8 | # You may remove this if you don't use go modules. 9 | - go mod tidy 10 | 11 | builds: 12 | - id: openapi-mcp 13 | main: ./cmd/openapi-mcp 14 | binary: openapi-mcp 15 | env: 16 | - CGO_ENABLED=0 17 | goos: 18 | - linux 19 | - windows 20 | - darwin 21 | goarch: 22 | - amd64 23 | - arm64 24 | ignore: 25 | - goos: windows 26 | goarch: arm64 27 | ldflags: 28 | - -s -w -X main.version={{.Version}} 29 | 30 | - id: mcp-client 31 | main: ./cmd/mcp-client 32 | binary: mcp-client 33 | env: 34 | - CGO_ENABLED=0 35 | goos: 36 | - linux 37 | - windows 38 | - darwin 39 | goarch: 40 | - amd64 41 | - arm64 42 | ignore: 43 | - goos: windows 44 | goarch: arm64 45 | ldflags: 46 | - -s -w -X main.version={{.Version}} 47 | 48 | archives: 49 | - id: default 50 | format: tar.gz 51 | name_template: >- 52 | {{ .ProjectName }}_ 53 | {{- title .Os }}_ 54 | {{- if eq .Arch "amd64" }}x86_64 55 | {{- else if eq .Arch "386" }}i386 56 | {{- else }}{{ .Arch }}{{ end }} 57 | format_overrides: 58 | - goos: windows 59 | format: zip 60 | 61 | checksum: 62 | name_template: 'checksums.txt' 63 | 64 | snapshot: 65 | name_template: "{{ incpatch .Version }}-next" 66 | 67 | changelog: 68 | sort: asc 69 | filters: 70 | exclude: 71 | - '^docs:' 72 | - '^test:' 73 | - '^ci:' 74 | - '^chore:' 75 | - Merge pull request 76 | - Merge branch 77 | 78 | # Release to GitHub 79 | release: 80 | github: 81 | owner: jedisct1 82 | name: openapi-mcp 83 | prerelease: auto 84 | draft: false 85 | name_template: "{{ .ProjectName }} v{{ .Version }}" 86 | header: | 87 | # {{ .ProjectName }} v{{ .Version }} 88 | 89 | ## What's New 90 | 91 | For a full list of changes, see the [changelog](CHANGELOG.md). -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Frank Denis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Binaries will be built into the ./bin directory 2 | .PHONY: all mcp-client openapi-mcp clean 3 | 4 | all: bin/mcp-client bin/openapi-mcp 5 | 6 | bin/mcp-client: $(shell find pkg -type f -name '*.go') $(shell find cmd/mcp-client -type f -name '*.go') 7 | @mkdir -p bin 8 | go build -o bin/mcp-client ./cmd/mcp-client 9 | 10 | bin/openapi-mcp: $(shell find pkg -type f -name '*.go') $(shell find cmd/openapi-mcp -type f -name '*.go') 11 | @mkdir -p bin 12 | go build -o bin/openapi-mcp ./cmd/openapi-mcp 13 | 14 | test: 15 | go test ./... 16 | 17 | clean: 18 | rm -f bin/mcp-client bin/openapi-mcp 19 | -------------------------------------------------------------------------------- /cmd/openapi-mcp/doc.go: -------------------------------------------------------------------------------- 1 | // doc.go 2 | package main 3 | 4 | import ( 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "strings" 11 | 12 | "github.com/getkin/kin-openapi/openapi3" 13 | "github.com/jedisct1/openapi-mcp/pkg/openapi2mcp" 14 | ) 15 | 16 | // handleDocMode handles the --doc mode, generating Markdown documentation for all tools. 17 | func handleDocMode(flags *cliFlags, ops []openapi2mcp.OpenAPIOperation, doc *openapi3.T) { 18 | toolSummaries := make([]map[string]any, 0, len(ops)) 19 | for _, op := range ops { 20 | name := op.OperationID 21 | if flags.toolNameFormat != "" { 22 | name = formatToolName(flags.toolNameFormat, name) 23 | } 24 | desc := op.Description 25 | if desc == "" { 26 | desc = op.Summary 27 | } 28 | inputSchema := openapi2mcp.BuildInputSchema(op.Parameters, op.RequestBody) 29 | toolSummaries = append(toolSummaries, map[string]any{ 30 | "name": name, 31 | "description": desc, 32 | "tags": op.Tags, 33 | "inputSchema": inputSchema, 34 | }) 35 | } 36 | jsonBytes, _ := json.MarshalIndent(toolSummaries, "", " ") 37 | if flags.postHookCmd != "" { 38 | out, err := processWithPostHook(jsonBytes, flags.postHookCmd) 39 | if err != nil { 40 | fmt.Fprintf(os.Stderr, "Error running post-hook-cmd: %v\n", err) 41 | os.Exit(1) 42 | } 43 | jsonBytes = out 44 | } 45 | if flags.docFormat == "markdown" { 46 | // Parse the possibly post-processed JSON back to []map[string]any 47 | var processed []map[string]any 48 | if err := json.Unmarshal(jsonBytes, &processed); err != nil { 49 | fmt.Fprintf(os.Stderr, "Error parsing post-processed JSON: %v\n", err) 50 | os.Exit(1) 51 | } 52 | if err := writeMarkdownDocFromSummaries(flags.docFile, processed, doc); err != nil { 53 | fmt.Fprintf(os.Stderr, "Error writing Markdown doc: %v\n", err) 54 | os.Exit(1) 55 | } 56 | fmt.Fprintf(os.Stderr, "Wrote Markdown documentation to %s\n", flags.docFile) 57 | os.Exit(0) 58 | } else if flags.docFormat == "html" { 59 | fmt.Fprintf(os.Stderr, "HTML documentation output is not yet implemented.\n") 60 | os.Exit(1) 61 | } else { 62 | fmt.Fprintf(os.Stderr, "Unknown doc format: %s\n", flags.docFormat) 63 | os.Exit(1) 64 | } 65 | } 66 | 67 | // writeMarkdownDocFromSummaries writes Markdown documentation from a []map[string]any (post-processed summaries). 68 | func writeMarkdownDocFromSummaries(path string, summaries []map[string]any, doc *openapi3.T) error { 69 | f, err := os.Create(path) 70 | if err != nil { 71 | return err 72 | } 73 | defer f.Close() 74 | f.WriteString("# MCP Tools Documentation\n\n") 75 | if doc.Info != nil { 76 | f.WriteString(fmt.Sprintf("**API Title:** %s\n\n", doc.Info.Title)) 77 | f.WriteString(fmt.Sprintf("**Version:** %s\n\n", doc.Info.Version)) 78 | if doc.Info.Description != "" { 79 | f.WriteString(doc.Info.Description + "\n\n") 80 | } 81 | } 82 | for _, m := range summaries { 83 | name, _ := m["name"].(string) 84 | desc, _ := m["description"].(string) 85 | tags, _ := m["tags"].([]any) 86 | inputSchema, _ := m["inputSchema"].(map[string]any) 87 | f.WriteString(fmt.Sprintf("## %s\n\n", name)) 88 | if desc != "" { 89 | f.WriteString(desc + "\n\n") 90 | } 91 | if len(tags) > 0 { 92 | tagStrs := make([]string, len(tags)) 93 | for i, t := range tags { 94 | tagStrs[i], _ = t.(string) 95 | } 96 | f.WriteString(fmt.Sprintf("**Tags:** %s\n\n", strings.Join(tagStrs, ", "))) 97 | } 98 | // Arguments 99 | props, _ := inputSchema["properties"].(map[string]any) 100 | if len(props) > 0 { 101 | f.WriteString("**Arguments:**\n\n") 102 | f.WriteString("| Name | Type | Description |\n|------|------|-------------|\n") 103 | for name, v := range props { 104 | vmap, _ := v.(map[string]any) 105 | typeStr, _ := vmap["type"].(string) 106 | desc, _ := vmap["description"].(string) 107 | f.WriteString(fmt.Sprintf("| %s | %s | %s |\n", name, typeStr, desc)) 108 | } 109 | f.WriteString("\n") 110 | } 111 | // Example call (best effort) 112 | example := map[string]any{} 113 | for name, v := range props { 114 | vmap, _ := v.(map[string]any) 115 | typeStr, _ := vmap["type"].(string) 116 | descStr, _ := vmap["description"].(string) 117 | if typeStr == "string" && strings.Contains(strings.ToLower(descStr), "integer") { 118 | example[name] = "123" 119 | continue 120 | } 121 | switch typeStr { 122 | case "string": 123 | example[name] = "example" 124 | case "number": 125 | example[name] = 123.45 126 | case "integer": 127 | example[name] = 123 128 | case "boolean": 129 | example[name] = true 130 | default: 131 | example[name] = "..." 132 | } 133 | } 134 | if len(example) > 0 { 135 | exampleJSON, _ := json.MarshalIndent(example, "", " ") 136 | f.WriteString("**Example call:**\n\n") 137 | f.WriteString("```json\n" + fmt.Sprintf("call %s %s\n", name, string(exampleJSON)) + "```\n\n") 138 | } 139 | } 140 | return nil 141 | } 142 | 143 | // processWithPostHook pipes JSON through an external command and returns the output. 144 | func processWithPostHook(jsonBytes []byte, postHookCmd string) ([]byte, error) { 145 | cmd := exec.Command("sh", "-c", postHookCmd) 146 | stdin, err := cmd.StdinPipe() 147 | if err != nil { 148 | return nil, err 149 | } 150 | stdout, err := cmd.StdoutPipe() 151 | if err != nil { 152 | return nil, err 153 | } 154 | errPipe, _ := cmd.StderrPipe() 155 | if err := cmd.Start(); err != nil { 156 | return nil, err 157 | } 158 | stdin.Write(jsonBytes) 159 | stdin.Close() 160 | out, _ := io.ReadAll(stdout) 161 | errBytes, _ := io.ReadAll(errPipe) 162 | err = cmd.Wait() 163 | if err != nil { 164 | return nil, fmt.Errorf("post-hook-cmd failed: %v\n%s", err, string(errBytes)) 165 | } 166 | return out, nil 167 | } 168 | 169 | // formatToolName applies the requested tool name formatting. 170 | func formatToolName(format, name string) string { 171 | switch format { 172 | case "lower": 173 | return strings.ToLower(name) 174 | case "upper": 175 | return strings.ToUpper(name) 176 | case "snake": 177 | return toSnakeCase(name) 178 | case "camel": 179 | return toCamelCase(name) 180 | default: 181 | return name 182 | } 183 | } 184 | 185 | // toSnakeCase converts a string to snake_case. 186 | func toSnakeCase(s string) string { 187 | var out []rune 188 | for i, r := range s { 189 | if i > 0 && r >= 'A' && r <= 'Z' { 190 | out = append(out, '_') 191 | } 192 | out = append(out, r) 193 | } 194 | return strings.ToLower(string(out)) 195 | } 196 | 197 | // toCamelCase converts a string to camelCase. 198 | func toCamelCase(s string) string { 199 | parts := strings.FieldsFunc(s, func(r rune) bool { 200 | return r == '_' || r == '-' || r == ' ' 201 | }) 202 | if len(parts) == 0 { 203 | return s 204 | } 205 | out := strings.ToLower(parts[0]) 206 | for _, p := range parts[1:] { 207 | if len(p) > 0 { 208 | out += strings.ToUpper(p[:1]) + strings.ToLower(p[1:]) 209 | } 210 | } 211 | return out 212 | } 213 | -------------------------------------------------------------------------------- /cmd/openapi-mcp/flags.go: -------------------------------------------------------------------------------- 1 | // flags.go 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | // cliFlags holds all parsed CLI flags and arguments. 12 | type cliFlags struct { 13 | showHelp bool 14 | extended bool 15 | quiet bool 16 | machine bool 17 | apiKeyFlag string 18 | baseURLFlag string 19 | bearerToken string 20 | basicAuth string 21 | httpAddr string 22 | httpTransport string // new: sse (default) or streamable 23 | includeDescRegex string 24 | excludeDescRegex string 25 | dryRun bool 26 | summary bool 27 | toolNameFormat string 28 | diffFile string 29 | tagFlags multiFlag 30 | docFile string 31 | docFormat string 32 | postHookCmd string 33 | noConfirmDangerous bool 34 | args []string 35 | mounts mountFlags // slice of mountFlag 36 | functionListFile string // Path to file listing functions to include (for filter command) 37 | } 38 | 39 | type mountFlag struct { 40 | BasePath string 41 | SpecPath string 42 | } 43 | 44 | type mountFlags []mountFlag 45 | 46 | func (m *mountFlags) String() string { 47 | return fmt.Sprintf("%v", *m) 48 | } 49 | 50 | func (m *mountFlags) Set(val string) error { 51 | // Expect format: /base:path/to/spec.yaml 52 | sep := strings.Index(val, ":") 53 | if sep < 1 || sep == len(val)-1 { 54 | return fmt.Errorf("invalid --mount value: %q (expected /base:path/to/spec.yaml)", val) 55 | } 56 | *m = append(*m, mountFlag{ 57 | BasePath: val[:sep], 58 | SpecPath: val[sep+1:], 59 | }) 60 | return nil 61 | } 62 | 63 | // parseFlags parses all CLI flags and returns a cliFlags struct. 64 | func parseFlags() *cliFlags { 65 | var flags cliFlags 66 | flag.BoolVar(&flags.showHelp, "h", false, "Show help") 67 | flag.BoolVar(&flags.showHelp, "help", false, "Show help") 68 | flag.BoolVar(&flags.extended, "extended", false, "Enable extended (human-friendly) output") 69 | // Default to minimal output 70 | flags.quiet = true 71 | flags.machine = true 72 | flag.StringVar(&flags.apiKeyFlag, "api-key", "", "API key for authenticated endpoints (overrides API_KEY env)") 73 | flag.StringVar(&flags.baseURLFlag, "base-url", "", "Override the base URL for HTTP calls (overrides OPENAPI_BASE_URL env)") 74 | flag.StringVar(&flags.bearerToken, "bearer-token", os.Getenv("BEARER_TOKEN"), "Bearer token for Authorization header (overrides BEARER_TOKEN env)") 75 | flag.StringVar(&flags.basicAuth, "basic-auth", os.Getenv("BASIC_AUTH"), "Basic auth (user:pass) for Authorization header (overrides BASIC_AUTH env)") 76 | flag.StringVar(&flags.httpAddr, "http", "", "Serve over HTTP on this address (e.g., :8080). For MCP server: serves tools via HTTP. For validate/lint: creates REST API endpoints.") 77 | flag.StringVar(&flags.httpTransport, "http-transport", "streamable", "HTTP transport to use for MCP server: 'streamable' (default) or 'sse'") 78 | flag.StringVar(&flags.includeDescRegex, "include-desc-regex", "", "Only include APIs whose description matches this regex (overrides INCLUDE_DESC_REGEX env)") 79 | flag.StringVar(&flags.excludeDescRegex, "exclude-desc-regex", "", "Exclude APIs whose description matches this regex (overrides EXCLUDE_DESC_REGEX env)") 80 | flag.BoolVar(&flags.dryRun, "dry-run", false, "Print the generated MCP tool schemas and exit (do not start the server)") 81 | flag.Var(&flags.tagFlags, "tag", "Only include tools with the given OpenAPI tag (repeatable)") 82 | flag.StringVar(&flags.toolNameFormat, "tool-name-format", "", "Format tool names: lower, upper, snake, camel") 83 | flag.BoolVar(&flags.summary, "summary", false, "Print a summary of the generated tools (count, tags, etc)") 84 | flag.StringVar(&flags.diffFile, "diff", "", "Compare the generated output to a previous run (file path)") 85 | flag.StringVar(&flags.docFile, "doc", "", "Write Markdown/HTML documentation for all tools to this file (implies no server)") 86 | flag.StringVar(&flags.docFormat, "doc-format", "markdown", "Documentation format: markdown (default) or html") 87 | flag.StringVar(&flags.postHookCmd, "post-hook-cmd", "", "Command to post-process the generated tool schema JSON (used in --dry-run or --doc mode)") 88 | flag.BoolVar(&flags.noConfirmDangerous, "no-confirm-dangerous", false, "Disable confirmation prompt for dangerous (PUT/POST/DELETE) actions in tool descriptions") 89 | flag.Var(&flags.mounts, "mount", "Mount an OpenAPI spec at a base path: /base:path/to/spec.yaml (repeatable, can be used multiple times)") 90 | flag.StringVar(&flags.functionListFile, "function-list-file", "", "File with list of function (operationId) names to include (one per line, for filter command)") 91 | flag.Parse() 92 | flags.args = flag.Args() 93 | if flags.extended { 94 | flags.quiet = false 95 | flags.machine = false 96 | } 97 | return &flags 98 | } 99 | 100 | // setEnvFromFlags sets environment variables from CLI flags if provided. 101 | func setEnvFromFlags(flags *cliFlags) { 102 | if flags.apiKeyFlag != "" { 103 | os.Setenv("API_KEY", flags.apiKeyFlag) 104 | } 105 | if flags.baseURLFlag != "" { 106 | os.Setenv("OPENAPI_BASE_URL", flags.baseURLFlag) 107 | } 108 | if flags.includeDescRegex != "" { 109 | os.Setenv("INCLUDE_DESC_REGEX", flags.includeDescRegex) 110 | } 111 | if flags.excludeDescRegex != "" { 112 | os.Setenv("EXCLUDE_DESC_REGEX", flags.excludeDescRegex) 113 | } 114 | } 115 | 116 | // printHelp prints the CLI help message. 117 | func printHelp() { 118 | fmt.Print(`openapi-mcp: Expose OpenAPI APIs as MCP tools 119 | 120 | Usage: 121 | openapi-mcp [flags] filter 122 | openapi-mcp [flags] validate 123 | openapi-mcp [flags] lint 124 | openapi-mcp [flags] 125 | 126 | Commands: 127 | filter Output a filtered list of operations as JSON, applying --tag, --include-desc-regex, --exclude-desc-regex, and --function-list-file (no server) 128 | validate Validate the OpenAPI spec and report actionable errors (with --http: starts validation API server) 129 | lint Perform detailed OpenAPI linting with comprehensive suggestions (with --http: starts linting API server) 130 | 131 | Examples: 132 | 133 | Basic MCP Server (stdio): 134 | openapi-mcp api.yaml # Start stdio MCP server 135 | openapi-mcp --api-key=key123 api.yaml # With API authentication 136 | 137 | MCP Server over HTTP (single API): 138 | openapi-mcp --http=:8080 api.yaml # HTTP server on port 8080 139 | openapi-mcp --http-transport=sse --http=:8080 api.yaml # Use SSE transport 140 | openapi-mcp --http=:8080 --extended api.yaml # With human-friendly output 141 | 142 | MCP Server over HTTP (multiple APIs): 143 | openapi-mcp --http=:8080 --mount /petstore:petstore.yaml --mount /books:books.yaml 144 | # Each API is served at its own base path (e.g., /petstore, /books) using StreamableHTTP by default 145 | # If --mount is used, positional OpenAPI spec arguments are ignored in HTTP mode. 146 | 147 | # With authentication via HTTP headers: 148 | curl -H "X-API-Key: your_key" http://localhost:8080/mcp -d '...' 149 | curl -H "Authorization: Bearer your_token" http://localhost:8080/mcp -d '...' 150 | 151 | Validation & Linting: 152 | openapi-mcp validate api.yaml # Check for critical issues 153 | openapi-mcp lint api.yaml # Comprehensive linting 154 | 155 | HTTP Validation/Linting Services: 156 | openapi-mcp --http=:8080 validate # REST API for validation 157 | openapi-mcp --http=:8080 lint # REST API for linting 158 | 159 | Filtering & Documentation: 160 | openapi-mcp filter --tag=admin api.yaml # Only admin operations 161 | openapi-mcp filter --dry-run api.yaml # Preview generated tools 162 | openapi-mcp filter --doc=tools.md api.yaml # Generate documentation 163 | openapi-mcp filter --tag=admin api.yaml # Output only admin-tagged operations as JSON 164 | openapi-mcp filter --include-desc-regex=foo api.yaml # Output operations whose description matches 'foo' 165 | openapi-mcp filter --function-list-file=funcs.txt api.yaml # Output only operations listed in funcs.txt 166 | 167 | Advanced Configuration: 168 | openapi-mcp --base-url=https://api.prod.com api.yaml # Override base URL 169 | openapi-mcp --include-desc-regex="user.*" api.yaml # Filter by description 170 | openapi-mcp --no-confirm-dangerous api.yaml # Skip confirmations 171 | openapi-mcp --http-transport=sse --http=:8080 api.yaml # Use SSE transport 172 | 173 | Flags: 174 | --extended Enable extended (human-friendly) output (default: minimal/agent) 175 | --api-key API key for authenticated endpoints 176 | --base-url Override the base URL for HTTP calls 177 | --bearer-token Bearer token for Authorization header 178 | --basic-auth Basic auth (user:pass) for Authorization header 179 | --http Serve over HTTP on this address (e.g., :8080). For MCP server: serves tools via HTTP. For validate/lint: creates REST API endpoints. 180 | In HTTP mode, authentication can also be provided via headers: 181 | X-API-Key, Api-Key (for API keys) 182 | Authorization: Bearer (for bearer tokens) 183 | Authorization: Basic (for basic auth) 184 | --http-transport HTTP transport to use for MCP server: 'streamable' (default) or 'sse' 185 | --include-desc-regex Only include APIs whose description matches this regex 186 | --exclude-desc-regex Exclude APIs whose description matches this regex 187 | --dry-run Print the generated MCP tool schemas as JSON and exit 188 | --doc Write Markdown/HTML documentation for all tools to this file 189 | --doc-format Documentation format: markdown (default) or html 190 | --post-hook-cmd Command to post-process the generated tool schema JSON 191 | --no-confirm-dangerous Disable confirmation for dangerous actions 192 | --summary Print a summary for CI 193 | --tag Only include tools with the given tag 194 | --diff Compare generated tools with a reference file 195 | --mount /base:path/to/spec.yaml Mount an OpenAPI spec at a base path (repeatable, can be used multiple times) 196 | --function-list-file File with list of function (operationId) names to include (one per line, for filter command) 197 | --help, -h Show help 198 | 199 | By default, output is minimal and agent-friendly. Use --extended for banners, help, and human-readable output. 200 | 201 | HTTP API Usage (for validate/lint commands): 202 | curl -X POST http://localhost:8080/validate \ 203 | -H "Content-Type: application/json" \ 204 | -d '{"openapi_spec": "..."}' 205 | 206 | # Endpoints: POST /validate, POST /lint, GET /health 207 | `) 208 | os.Exit(0) 209 | } 210 | 211 | // multiFlag is a custom flag type for collecting repeated string values. 212 | type multiFlag []string 213 | 214 | // String returns the string representation of the multiFlag. 215 | func (m *multiFlag) String() string { 216 | return fmt.Sprintf("%v", *m) 217 | } 218 | 219 | // Set appends a value to the multiFlag. 220 | func (m *multiFlag) Set(val string) error { 221 | *m = append(*m, val) 222 | return nil 223 | } 224 | -------------------------------------------------------------------------------- /cmd/openapi-mcp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/jedisct1/openapi-mcp/pkg/openapi2mcp" 11 | "gopkg.in/yaml.v3" 12 | ) 13 | 14 | // main is the entrypoint for the openapi-mcp CLI. 15 | // It parses flags, loads the OpenAPI spec, and dispatches to the appropriate mode (server, doc, dry-run, etc). 16 | func main() { 17 | flags := parseFlags() 18 | 19 | if flags.showHelp { 20 | printHelp() 21 | os.Exit(0) 22 | } 23 | 24 | // Set env vars from flags if provided 25 | setEnvFromFlags(flags) 26 | 27 | args := flags.args 28 | 29 | // If --mount is used with --http, do not require a positional argument 30 | if flags.httpAddr != "" && len(flags.mounts) > 0 { 31 | if len(args) > 0 { 32 | fmt.Fprintln(os.Stderr, "[WARN] Positional OpenAPI spec arguments are ignored when using --mount. Only --mount will be used.") 33 | } 34 | startServer(flags, nil, nil) 35 | return 36 | } 37 | 38 | if len(args) < 1 { 39 | fmt.Fprintln(os.Stderr, "Error: missing required argument.") 40 | printHelp() 41 | os.Exit(1) 42 | } 43 | 44 | // Enforce: --lint (and all flags) must come before 'validate' command 45 | for i, arg := range os.Args[1:] { 46 | if arg == "validate" { 47 | for _, after := range os.Args[i+2:] { 48 | if after == "--lint" { 49 | fmt.Fprintln(os.Stderr, "Error: --lint must be specified before the 'validate' command.") 50 | fmt.Fprintln(os.Stderr, "Usage: openapi-mcp --lint validate ") 51 | os.Exit(1) 52 | } 53 | } 54 | } 55 | } 56 | 57 | // --- Validate subcommand --- 58 | if args[0] == "validate" { 59 | // Check if HTTP mode is requested 60 | if flags.httpAddr != "" { 61 | fmt.Fprintf(os.Stderr, "Starting OpenAPI validation HTTP server on %s\n", flags.httpAddr) 62 | err := openapi2mcp.ServeHTTPLint(flags.httpAddr, false) 63 | if err != nil { 64 | fmt.Fprintf(os.Stderr, "HTTP server failed: %v\n", err) 65 | os.Exit(1) 66 | } 67 | return 68 | } 69 | 70 | if len(args) < 2 { 71 | fmt.Fprintln(os.Stderr, "Error: missing required argument for validate.") 72 | os.Exit(1) 73 | } 74 | specPath := args[1] 75 | doc, err := openapi2mcp.LoadOpenAPISpec(specPath) 76 | if err != nil { 77 | fmt.Fprintf(os.Stderr, "Validation failed: %v\n", err) 78 | os.Exit(1) 79 | } 80 | fmt.Fprintln(os.Stderr, "OpenAPI spec loaded and validated successfully.") 81 | // Run MCP self-test for actionable errors 82 | // We'll simulate tool names as if all operationIds are present 83 | ops := openapi2mcp.ExtractOpenAPIOperations(doc) 84 | var toolNames []string 85 | for _, op := range ops { 86 | toolNames = append(toolNames, op.OperationID) 87 | } 88 | err = openapi2mcp.SelfTestOpenAPIMCPWithOptions(doc, toolNames, false) 89 | if err != nil { 90 | fmt.Fprintf(os.Stderr, "MCP self-test failed: %v\n", err) 91 | os.Exit(1) 92 | } 93 | fmt.Fprintln(os.Stderr, "MCP self-test passed: all tools and required arguments are present.") 94 | os.Exit(0) 95 | } 96 | // --- End validate subcommand --- 97 | 98 | // --- Lint subcommand --- 99 | if args[0] == "lint" { 100 | // Check if HTTP mode is requested 101 | if flags.httpAddr != "" { 102 | fmt.Fprintf(os.Stderr, "Starting OpenAPI linting HTTP server on %s\n", flags.httpAddr) 103 | err := openapi2mcp.ServeHTTPLint(flags.httpAddr, true) 104 | if err != nil { 105 | fmt.Fprintf(os.Stderr, "HTTP server failed: %v\n", err) 106 | os.Exit(1) 107 | } 108 | return 109 | } 110 | 111 | if len(args) < 2 { 112 | fmt.Fprintln(os.Stderr, "Error: missing required argument for lint.") 113 | os.Exit(1) 114 | } 115 | specPath := args[1] 116 | doc, err := openapi2mcp.LoadOpenAPISpec(specPath) 117 | if err != nil { 118 | fmt.Fprintf(os.Stderr, "Linting failed: %v\n", err) 119 | os.Exit(1) 120 | } 121 | fmt.Fprintln(os.Stderr, "OpenAPI spec loaded successfully.") 122 | // Run detailed MCP linting with comprehensive suggestions 123 | ops := openapi2mcp.ExtractOpenAPIOperations(doc) 124 | var toolNames []string 125 | for _, op := range ops { 126 | toolNames = append(toolNames, op.OperationID) 127 | } 128 | err = openapi2mcp.SelfTestOpenAPIMCPWithOptions(doc, toolNames, true) 129 | if err != nil { 130 | fmt.Fprintf(os.Stderr, "OpenAPI linting completed with issues: %v\n", err) 131 | os.Exit(1) 132 | } 133 | fmt.Fprintln(os.Stderr, "OpenAPI linting passed: spec follows all best practices.") 134 | os.Exit(0) 135 | } 136 | // --- End lint subcommand --- 137 | 138 | // --- Filter subcommand --- 139 | if args[0] == "filter" { 140 | if len(args) < 2 { 141 | fmt.Fprintln(os.Stderr, "Error: missing required argument for filter.") 142 | os.Exit(1) 143 | } 144 | specPath := args[1] 145 | doc, err := openapi2mcp.LoadOpenAPISpec(specPath) 146 | if err != nil { 147 | fmt.Fprintf(os.Stderr, "Error: Could not load OpenAPI spec: %v\n", err) 148 | os.Exit(1) 149 | } 150 | 151 | // Compile regex filters if provided 152 | var includeRegex, excludeRegex *regexp.Regexp 153 | if val := os.Getenv("INCLUDE_DESC_REGEX"); val != "" { 154 | includeRegex, err = regexp.Compile(val) 155 | if err != nil { 156 | fmt.Fprintf(os.Stderr, "Error: Invalid INCLUDE_DESC_REGEX: %v\n", err) 157 | os.Exit(1) 158 | } 159 | } 160 | if val := os.Getenv("EXCLUDE_DESC_REGEX"); val != "" { 161 | excludeRegex, err = regexp.Compile(val) 162 | if err != nil { 163 | fmt.Fprintf(os.Stderr, "Error: Invalid EXCLUDE_DESC_REGEX: %v\n", err) 164 | os.Exit(1) 165 | } 166 | } 167 | 168 | ops := openapi2mcp.ExtractFilteredOpenAPIOperations(doc, includeRegex, excludeRegex) 169 | // Apply tag filter if present 170 | if len(flags.tagFlags) > 0 { 171 | var filtered []openapi2mcp.OpenAPIOperation 172 | for _, op := range ops { 173 | found := false 174 | for _, tag := range op.Tags { 175 | for _, want := range flags.tagFlags { 176 | if tag == want { 177 | found = true 178 | break 179 | } 180 | } 181 | if found { 182 | break 183 | } 184 | } 185 | if found { 186 | filtered = append(filtered, op) 187 | } 188 | } 189 | ops = filtered 190 | } 191 | // Apply function list file filter if present 192 | if flags.functionListFile != "" { 193 | funcNames := make(map[string]struct{}) 194 | data, err := os.ReadFile(flags.functionListFile) 195 | if err != nil { 196 | fmt.Fprintf(os.Stderr, "Error: Could not read function list file: %v\n", err) 197 | os.Exit(1) 198 | } 199 | for _, line := range regexp.MustCompile(`\r?\n`).Split(string(data), -1) { 200 | line = regexp.MustCompile(`^\s+|\s+$`).ReplaceAllString(line, "") 201 | if line != "" { 202 | funcNames[line] = struct{}{} 203 | } 204 | } 205 | var filtered []openapi2mcp.OpenAPIOperation 206 | for _, op := range ops { 207 | if _, ok := funcNames[op.OperationID]; ok { 208 | filtered = append(filtered, op) 209 | } 210 | } 211 | ops = filtered 212 | } 213 | 214 | // Patch doc.Paths to only include filtered operations 215 | if len(ops) == 0 { 216 | // If no operations remain after filtering, clear all paths 217 | for path := range doc.Paths.Map() { 218 | doc.Paths.Delete(path) 219 | } 220 | } else { 221 | opMap := make(map[string]map[string]struct{}) // path -> method -> present 222 | for _, op := range ops { 223 | if _, ok := opMap[op.Path]; !ok { 224 | opMap[op.Path] = make(map[string]struct{}) 225 | } 226 | opMap[op.Path][strings.ToLower(op.Method)] = struct{}{} 227 | } 228 | for path, pathItem := range doc.Paths.Map() { 229 | // Remove methods not in opMap 230 | for method := range pathItem.Operations() { 231 | if _, ok := opMap[path][strings.ToLower(method)]; !ok { 232 | // Remove this method from the PathItem 233 | switch strings.ToLower(method) { 234 | case "get": 235 | pathItem.Get = nil 236 | case "put": 237 | pathItem.Put = nil 238 | case "post": 239 | pathItem.Post = nil 240 | case "delete": 241 | pathItem.Delete = nil 242 | case "options": 243 | pathItem.Options = nil 244 | case "head": 245 | pathItem.Head = nil 246 | case "patch": 247 | pathItem.Patch = nil 248 | case "trace": 249 | pathItem.Trace = nil 250 | } 251 | } 252 | } 253 | // If all methods are nil, remove the path entirely 254 | hasOp := false 255 | for _, op := range pathItem.Operations() { 256 | if op != nil { 257 | hasOp = true 258 | break 259 | } 260 | } 261 | if !hasOp { 262 | doc.Paths.Delete(path) 263 | } 264 | } 265 | } 266 | 267 | // Output the filtered OpenAPI spec as a valid OpenAPI file using kin-openapi's marshaling 268 | ext := "" 269 | if dot := len(specPath) - 1 - len(specPath); dot >= 0 { 270 | ext = "" 271 | } else { 272 | dot = len(specPath) - 1 273 | for i := len(specPath) - 1; i >= 0; i-- { 274 | if specPath[i] == '.' { 275 | dot = i 276 | break 277 | } 278 | } 279 | if dot < len(specPath)-1 { 280 | ext = specPath[dot+1:] 281 | } 282 | } 283 | ext = strings.ToLower(ext) 284 | if ext == "yaml" || ext == "yml" { 285 | // Output as YAML using kin-openapi's MarshalYAML 286 | yamlVal, err := doc.MarshalYAML() 287 | if err != nil { 288 | fmt.Fprintf(os.Stderr, "Error: Failed to marshal OpenAPI as YAML: %v\n", err) 289 | os.Exit(1) 290 | } 291 | switch v := yamlVal.(type) { 292 | case []byte: 293 | fmt.Print(string(v)) 294 | default: 295 | // Fallback: use yaml.v3 Marshal if needed 296 | b, err := yaml.Marshal(v) 297 | if err != nil { 298 | fmt.Fprintf(os.Stderr, "Error: Failed to marshal YAML fallback: %v\n", err) 299 | os.Exit(1) 300 | } 301 | fmt.Print(string(b)) 302 | } 303 | } else { 304 | // Output as JSON using encoding/json 305 | jsonBytes, err := json.MarshalIndent(doc, "", " ") 306 | if err != nil { 307 | fmt.Fprintf(os.Stderr, "Error: Failed to marshal OpenAPI as JSON: %v\n", err) 308 | os.Exit(1) 309 | } 310 | fmt.Println(string(jsonBytes)) 311 | } 312 | os.Exit(0) 313 | } 314 | 315 | specPath := args[len(args)-1] 316 | doc, err := openapi2mcp.LoadOpenAPISpec(specPath) 317 | if err != nil { 318 | fmt.Fprintf(os.Stderr, "Error: Could not load OpenAPI spec: %v\n", err) 319 | os.Exit(1) 320 | } 321 | fmt.Fprintln(os.Stderr, "OpenAPI spec loaded and validated successfully.") 322 | 323 | // Compile regex filters if provided 324 | var includeRegex, excludeRegex *regexp.Regexp 325 | if val := os.Getenv("INCLUDE_DESC_REGEX"); val != "" { 326 | includeRegex, err = regexp.Compile(val) 327 | if err != nil { 328 | fmt.Fprintf(os.Stderr, "Error: Invalid INCLUDE_DESC_REGEX: %v\n", err) 329 | os.Exit(1) 330 | } 331 | } 332 | if val := os.Getenv("EXCLUDE_DESC_REGEX"); val != "" { 333 | excludeRegex, err = regexp.Compile(val) 334 | if err != nil { 335 | fmt.Fprintf(os.Stderr, "Error: Invalid EXCLUDE_DESC_REGEX: %v\n", err) 336 | os.Exit(1) 337 | } 338 | } 339 | 340 | ops := openapi2mcp.ExtractFilteredOpenAPIOperations(doc, includeRegex, excludeRegex) 341 | 342 | // Dispatch to doc, dry-run, or server mode 343 | if flags.docFile != "" { 344 | handleDocMode(flags, ops, doc) 345 | return 346 | } 347 | if flags.dryRun { 348 | handleDryRunMode(flags, ops, doc) 349 | return 350 | } 351 | startServer(flags, ops, doc) 352 | } 353 | 354 | // handleDocMode handles the --doc mode, generating documentation for all tools. 355 | // func handleDocMode(flags *cliFlags, ops []openapi2mcp.OpenAPIOperation, doc *openapi3.T) { 356 | // // Implementation in doc.go 357 | // panic("handleDocMode not yet implemented") 358 | // } 359 | 360 | // handleDryRunMode handles the --dry-run mode, printing tool schemas and summaries. 361 | // func handleDryRunMode(flags *cliFlags, ops []openapi2mcp.OpenAPIOperation, doc *openapi3.T) { 362 | // // Implementation in utils.go or a dedicated file 363 | // panic("handleDryRunMode not yet implemented") 364 | // } 365 | -------------------------------------------------------------------------------- /cmd/openapi-mcp/server.go: -------------------------------------------------------------------------------- 1 | // server.go 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | 10 | "github.com/getkin/kin-openapi/openapi3" 11 | mcpserver "github.com/jedisct1/openapi-mcp/pkg/mcp/server" 12 | "github.com/jedisct1/openapi-mcp/pkg/openapi2mcp" 13 | ) 14 | 15 | // startServer starts the MCP server in stdio or HTTP mode, based on CLI flags. 16 | // It registers all OpenAPI operations as MCP tools and starts the server. 17 | func startServer(flags *cliFlags, ops []openapi2mcp.OpenAPIOperation, doc *openapi3.T) { 18 | if flags.httpAddr != "" && len(flags.mounts) > 0 { 19 | // Check for duplicate base paths 20 | basePathCount := make(map[string]int) 21 | for _, m := range flags.mounts { 22 | basePathCount[m.BasePath]++ 23 | } 24 | var dups []string 25 | for base, count := range basePathCount { 26 | if count > 1 { 27 | dups = append(dups, base) 28 | } 29 | } 30 | if len(dups) > 0 { 31 | fmt.Fprintf(os.Stderr, "Error: duplicate --mount base path(s): %v\nEach base path may only be used once.\n", dups) 32 | os.Exit(2) 33 | } 34 | if len(flags.args) > 0 { 35 | fmt.Fprintln(os.Stderr, "[WARN] Positional OpenAPI spec arguments are ignored when using --mount. Only --mount will be used.") 36 | } 37 | mux := http.NewServeMux() 38 | for _, m := range flags.mounts { 39 | fmt.Fprintf(os.Stderr, "Loading OpenAPI spec for mount %s: %s...\n", m.BasePath, m.SpecPath) 40 | d, err := openapi3.NewLoader().LoadFromFile(m.SpecPath) 41 | if err != nil { 42 | log.Fatalf("Failed to load OpenAPI spec for %s: %v", m.BasePath, err) 43 | } 44 | ops = openapi2mcp.ExtractOpenAPIOperations(d) 45 | srv := openapi2mcp.NewServerWithOps("openapi-mcp", d.Info.Version, d, ops) 46 | var handler http.Handler 47 | if flags.httpTransport == "streamable" { 48 | handler = openapi2mcp.HandlerForStreamableHTTP(srv, m.BasePath) 49 | } else { 50 | handler = openapi2mcp.HandlerForBasePath(srv, m.BasePath) 51 | } 52 | mux.Handle(m.BasePath+"/", handler) 53 | mux.Handle(m.BasePath, handler) // allow both /base and /base/ 54 | fmt.Fprintf(os.Stderr, "Mounted %s at %s\n", m.SpecPath, m.BasePath) 55 | } 56 | fmt.Fprintf(os.Stderr, "Starting multi-mount MCP HTTP server on %s...\n", flags.httpAddr) 57 | if err := http.ListenAndServe(flags.httpAddr, mux); err != nil { 58 | log.Fatalf("Failed to start MCP HTTP server: %v", err) 59 | } 60 | return 61 | } 62 | 63 | if flags.httpAddr != "" { 64 | if len(flags.args) != 1 { 65 | fmt.Fprintln(os.Stderr, "Usage: openapi-mcp --http=:8080 ") 66 | os.Exit(2) 67 | } 68 | specPath := flags.args[0] 69 | d, err := openapi3.NewLoader().LoadFromFile(specPath) 70 | if err != nil { 71 | log.Fatalf("Failed to load OpenAPI spec: %v", err) 72 | } 73 | ops := openapi2mcp.ExtractOpenAPIOperations(d) 74 | srv := openapi2mcp.NewServerWithOps("openapi-mcp", d.Info.Version, d, ops) 75 | fmt.Fprintf(os.Stderr, "Starting MCP server (HTTP, %s transport) on %s...\n", flags.httpTransport, flags.httpAddr) 76 | if flags.httpTransport == "streamable" { 77 | if err := openapi2mcp.ServeStreamableHTTP(srv, flags.httpAddr, "/mcp"); err != nil { 78 | log.Fatalf("Failed to start MCP HTTP server: %v", err) 79 | } 80 | } else { 81 | if err := openapi2mcp.ServeHTTP(srv, flags.httpAddr, "/mcp"); err != nil { 82 | log.Fatalf("Failed to start MCP HTTP server: %v", err) 83 | } 84 | } 85 | return 86 | } 87 | 88 | // stdio mode: require a single positional OpenAPI spec argument 89 | if len(flags.args) != 1 { 90 | fmt.Fprintln(os.Stderr, "Usage: openapi-mcp ") 91 | os.Exit(2) 92 | } 93 | specPath := flags.args[0] 94 | d, err := openapi3.NewLoader().LoadFromFile(specPath) 95 | if err != nil { 96 | log.Fatalf("Failed to load OpenAPI spec: %v", err) 97 | } 98 | ops = openapi2mcp.ExtractOpenAPIOperations(d) 99 | srv := openapi2mcp.NewServerWithOps("openapi-mcp", d.Info.Version, d, ops) 100 | fmt.Fprintln(os.Stderr, "Registered all OpenAPI operations as MCP tools.") 101 | fmt.Fprintln(os.Stderr, "Starting MCP server (stdio)...") 102 | if err := openapi2mcp.ServeStdio(srv); err != nil { 103 | log.Fatalf("Failed to start MCP server: %v", err) 104 | } 105 | } 106 | 107 | // makeMCPHandler returns an http.Handler that serves the MCP server at the given basePath. 108 | func makeMCPHandler(srv *mcpserver.MCPServer, basePath string) http.Handler { 109 | return openapi2mcp.HandlerForBasePath(srv, basePath) 110 | } 111 | -------------------------------------------------------------------------------- /cmd/openapi-mcp/utils.go: -------------------------------------------------------------------------------- 1 | // utils.go 2 | package main 3 | 4 | import ( 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | 10 | "github.com/getkin/kin-openapi/openapi3" 11 | "github.com/jedisct1/openapi-mcp/pkg/openapi2mcp" 12 | ) 13 | 14 | // handleDryRunMode handles the --dry-run mode, printing tool schemas and summaries. 15 | func handleDryRunMode(flags *cliFlags, ops []openapi2mcp.OpenAPIOperation, doc *openapi3.T) { 16 | opts := &openapi2mcp.ToolGenOptions{ 17 | NameFormat: nil, // Not used for dry-run output 18 | TagFilter: flags.tagFlags, 19 | DryRun: true, 20 | PrettyPrint: true, 21 | Version: doc.Info.Version, 22 | ConfirmDangerousActions: !flags.noConfirmDangerous, 23 | } 24 | openapi2mcp.RegisterOpenAPITools(nil, ops, doc, opts) 25 | if flags.summary { 26 | openapi2mcp.PrintToolSummary(ops) 27 | } 28 | if flags.diffFile != "" { 29 | compareWithDiffFile(opts, doc, ops, flags.diffFile) 30 | } 31 | os.Exit(0) 32 | } 33 | 34 | // compareWithDiffFile compares the generated output to a previous run (file path). 35 | func compareWithDiffFile(opts *openapi2mcp.ToolGenOptions, doc *openapi3.T, ops []openapi2mcp.OpenAPIOperation, diffFile string) { 36 | // Generate current output 37 | var toolSummaries []map[string]any 38 | for _, op := range ops { 39 | if len(opts.TagFilter) > 0 { 40 | found := false 41 | for _, tag := range op.Tags { 42 | for _, want := range opts.TagFilter { 43 | if tag == want { 44 | found = true 45 | break 46 | } 47 | } 48 | } 49 | if !found { 50 | continue 51 | } 52 | } 53 | name := op.OperationID 54 | if opts.NameFormat != nil { 55 | name = opts.NameFormat(name) 56 | } 57 | desc := op.Description 58 | if desc == "" { 59 | desc = op.Summary 60 | } 61 | inputSchema := openapi2mcp.BuildInputSchema(op.Parameters, op.RequestBody) 62 | toolSummaries = append(toolSummaries, map[string]any{ 63 | "name": name, 64 | "description": desc, 65 | "tags": op.Tags, 66 | "inputSchema": inputSchema, 67 | }) 68 | } 69 | curBytes, _ := json.MarshalIndent(toolSummaries, "", " ") 70 | _, err := os.ReadFile(diffFile) 71 | if err != nil { 72 | fmt.Fprintf(os.Stderr, "Error: Could not read diff file: %v\n", err) 73 | return 74 | } 75 | tmpFile, err := os.CreateTemp("", "openapi2mcp-diff-*.json") 76 | if err != nil { 77 | fmt.Fprintf(os.Stderr, "Error: Could not create temp file for diff: %v\n", err) 78 | return 79 | } 80 | defer os.Remove(tmpFile.Name()) 81 | tmpFile.Write(curBytes) 82 | tmpFile.Close() 83 | cmd := exec.Command("diff", "-u", diffFile, tmpFile.Name()) 84 | cmd.Stdout = os.Stdout 85 | cmd.Stderr = os.Stderr 86 | err = cmd.Run() 87 | if err != nil && err.Error() != "exit status 1" { 88 | fmt.Fprintf(os.Stderr, "Error running diff: %v\n", err) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /docs/docs/cli.md: -------------------------------------------------------------------------------- 1 | ## Commands 2 | 3 | - `openapi-mcp [flags] `: Start the MCP server (stdio or HTTP) 4 | - `openapi-mcp validate `: Validate the OpenAPI spec and report actionable errors 5 | - `openapi-mcp lint `: Perform detailed OpenAPI linting with comprehensive suggestions 6 | - `openapi-mcp filter `: Output a filtered list of operations as JSON, applying `--tag`, `--include-desc-regex`, `--exclude-desc-regex`, and `--function-list-file` (no server) 7 | 8 | ## Usage 9 | 10 | ```sh 11 | openapi-mcp [flags] 12 | openapi-mcp validate 13 | openapi-mcp lint 14 | openapi-mcp filter [flags] 15 | openapi-mcp --http=:8080 --mount /petstore:petstore.yaml --mount /books:books.yaml 16 | ``` 17 | 18 | ## Examples 19 | 20 | ### Start MCP Server (stdio) 21 | ```sh 22 | openapi-mcp api.yaml 23 | ``` 24 | 25 | ### Start MCP Server over HTTP (single API) 26 | ```sh 27 | openapi-mcp --http=:8080 api.yaml 28 | ``` 29 | 30 | ### Start MCP Server over HTTP (multiple APIs) 31 | ```sh 32 | openapi-mcp --http=:8080 --mount /petstore:petstore.yaml --mount /books:books.yaml 33 | ``` 34 | By default, this will serve the Petstore API at `/petstore` (StreamableHTTP), and the Books API at `/books`. If you use `--http-transport=sse`, endpoints like `/petstore/sse` and `/petstore/message` will be available for SSE clients. 35 | 36 | ### Validate an OpenAPI Spec 37 | ```sh 38 | openapi-mcp validate api.yaml 39 | ``` 40 | 41 | ### Lint an OpenAPI Spec 42 | ```sh 43 | openapi-mcp lint api.yaml 44 | ``` 45 | 46 | ### Filter Operations by Tag, Description, or Function List 47 | ```sh 48 | openapi-mcp filter --tag=admin api.yaml 49 | openapi-mcp filter --include-desc-regex=foo api.yaml 50 | openapi-mcp filter --tag=admin --include-desc-regex=foo api.yaml 51 | openapi-mcp filter --function-list-file=funcs.txt api.yaml 52 | ``` 53 | You can use `--function-list-file=funcs.txt` to restrict the output to only the operations whose `operationId` is listed (one per line) in the given file. This filter is applied after tag and description filters. 54 | 55 | This will output a JSON array of operations matching the filters, including their name, description, tags, and input schema. -------------------------------------------------------------------------------- /docs/docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Documentation - openapi-mcp 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 44 | 45 | 46 |
47 |
48 | 49 | 65 | 66 | 67 |
68 |

openapi-mcp Documentation

69 | 70 |

71 | Welcome to the official documentation for openapi-mcp, a tool that transforms any OpenAPI 3.x specification into a powerful, AI-friendly MCP (Model Context Protocol) tool server. 72 |

73 | 74 |

What is openapi-mcp?

75 |

76 | openapi-mcp is a tool that loads and validates OpenAPI specifications (YAML or JSON), generates MCP tools for each operation, and starts an MCP stdio or HTTP server. This enables AI agents and automation to interact with any API defined by an OpenAPI spec. 77 |

78 | 79 |

80 | In essence, it bridges the gap between API definitions and AI tools, making any API accessible to AI agents, LLMs, and automation tools with consistent, structured output. 81 |

82 | 83 |

Key Features

84 |
    85 |
  • Instant API to MCP Conversion: Parses any OpenAPI 3.x YAML/JSON spec and generates MCP tools
  • 86 |
  • Multiple Transport Options: Supports stdio (default) and HTTP server modes (StreamableHTTP is default for HTTP, SSE also available)
  • 87 |
  • Complete Parameter Support: Path, query, header, cookie, and body parameters
  • 88 |
  • Authentication: API key, Bearer token, Basic auth, and OAuth2 support
  • 89 |
  • Structured Output: All responses have consistent, well-structured formats with type information
  • 90 |
  • Validation: All tool calls are validated against OpenAPI schemas before execution
  • 91 |
  • Safety Features: Confirmation required for dangerous operations (PUT/POST/DELETE)
  • 92 |
  • Documentation: Built-in documentation generation in Markdown or HTML
  • 93 |
  • Agent-Friendly: Designed specifically for AI agents with structured errors, hints, and examples
  • 94 |
  • Interactive Client: Includes an MCP client with readline support and command history
  • 95 |
  • Flexible Configuration: Environment variables or command-line flags
  • 96 |
  • CI/Testing Support: Summary options, exit codes, and dry-run mode
  • 97 |
98 | 99 |

AI Agent Integration

100 |

101 | openapi-mcp is designed to make any OpenAPI-documented API accessible to AI coding agents, editors, and LLM-based tools (such as Cursor, Copilot, GPT-4, and others). By exposing each API operation as a machine-readable tool with rich schemas, examples, and structured output, agents can: 102 |

103 | 104 |
    105 |
  • Discover available operations and their arguments
  • 106 |
  • Validate and construct correct API calls
  • 107 |
  • Handle errors, confirmations, and streaming output
  • 108 |
  • Chain multiple API calls together in workflows
  • 109 |
110 | 111 |
112 |

Quick Example

113 |

Run an MCP server for an OpenAPI spec:

114 |
bin/openapi-mcp examples/fastly-openapi-mcp.yaml
115 | 116 |

Use the interactive client to explore the API:

117 |
bin/mcp-client bin/openapi-mcp examples/fastly-openapi-mcp.yaml
118 | 119 |

Client session:

120 |
mcp> list
121 | ["listServices", "createService", "getService", "updateService", "deleteService", ...]
122 | 
123 | mcp> schema createService
124 | Schema for createService:
125 | {
126 |   "type": "object",
127 |   "properties": {
128 |     "name": {"type": "string", "description": "The name of the service."},
129 |     "comment": {"type": "string", "description": "A freeform descriptive note."},
130 |     "type": {"type": "string", "enum": ["vcl", "wasm"], "description": "The type of this service."}
131 |   },
132 |   "required": ["name", "type"]
133 | }
134 | Example: call createService {"name": "My Service", "type": "vcl"}
135 | 
136 | mcp> call createService {"name": "My Service", "type": "vcl"}
137 | HTTP POST /service
138 | Status: 200
139 | Response:
140 | {
141 |   "id": "SU1Z0isxPaozGVKXdv0eY",
142 |   "name": "My Service",
143 |   "customer_id": "x4xCwxxJxGCx123Rx5xTx",
144 |   "type": "vcl",
145 |   "created_at": "2016-08-17T19:27:28+00:00",
146 |   "updated_at": "2016-08-17T19:27:28+00:00"
147 | }
148 |
149 | 150 |

Next Steps

151 |

To get started with openapi-mcp, check out the following sections:

152 | 158 |
159 |
160 |
161 | 162 | 163 |
164 |
165 | 190 | 191 | 194 |
195 |
196 | 197 | 198 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /docs/docs/installation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Installation - openapi-mcp Documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 44 | 45 | 46 |
47 |
48 | 49 | 65 | 66 | 67 |
68 |

Installation

69 | 70 |

71 | This guide will walk you through installing and building openapi-mcp from source. 72 |

73 | 74 |

Prerequisites

75 |

Before you begin, ensure you have the following prerequisites:

76 |
    77 |
  • Go 1.21 or higher (Download Go)
  • 78 |
  • Git for cloning the repository
  • 79 |
  • An OpenAPI 3.x YAML or JSON specification file that you want to expose as MCP tools
  • 80 |
81 | 82 |

Build from Source

83 |

84 | To build openapi-mcp from source, follow these steps: 85 |

86 | 87 |

1. Clone the Repository

88 |
git clone https://github.com/jedisct1/openapi-mcp.git
 89 | cd openapi-mcp
90 | 91 |

2. Build the Binaries

92 |

93 | openapi-mcp uses a Makefile to simplify the build process. To build all binaries, run: 94 |

95 |
make
96 | 97 |

98 | This will place the command-line tools in the bin directory: 99 |

100 |
    101 |
  • bin/openapi-mcp - Main tool that processes OpenAPI specs and serves MCP
  • 102 |
  • bin/mcp-client - Interactive client for testing MCP tools
  • 103 |
104 | 105 |

106 | To build specific binaries: 107 |

108 |
# Build only the main tool
109 | make bin/openapi-mcp
110 | 
111 | # Build only the client
112 | make bin/mcp-client
113 | 114 |

3. Clean Build Artifacts (Optional)

115 |

116 | To clean build artifacts: 117 |

118 |
make clean
119 | 120 |

Verifying Installation

121 |

122 | To verify that the installation was successful, run: 123 |

124 |
bin/openapi-mcp --help
125 | 126 |

127 | You should see the help message with available command-line options. 128 |

129 | 130 |

Using Pre-built Binaries (When Available)

131 |

132 | For some platforms, pre-built binaries may be available from the 133 | GitHub releases page. 134 |

135 |

136 | Download the appropriate binary for your platform, extract it, and place it in your PATH. 137 |

138 | 139 |

Go Library Installation

140 |

141 | To use openapi-mcp as a Go library in your own applications: 142 |

143 | 144 |

Main Library

145 |

146 | For OpenAPI to MCP conversion functionality: 147 |

148 |
go get github.com/jedisct1/openapi-mcp/pkg/openapi2mcp
149 | 150 |

MCP Package

151 |

152 | For direct access to MCP server functionality: 153 |

154 |
go get github.com/jedisct1/openapi-mcp/pkg/mcp/server
155 | 156 |

157 | For MCP types and utilities: 158 |

159 |
go get github.com/jedisct1/openapi-mcp/pkg/mcp/mcp
160 | 161 |

162 | See the Library Usage documentation for detailed examples and API reference. 163 |

164 | 165 |

Next Steps

166 |

167 | Now that you have installed openapi-mcp, you can: 168 |

169 | 175 |
176 |
177 |
178 | 179 | 180 |
181 |
182 | 207 | 208 | 211 |
212 |
213 | 214 | 215 | 216 | 217 | 218 | -------------------------------------------------------------------------------- /docs/docs/quick-start.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Quick Start - openapi-mcp Documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 44 | 45 | 46 |
47 |
48 | 49 | 65 | 66 | 67 |
68 |

Quick Start

69 | 70 |

71 | This guide will help you quickly get up and running with openapi-mcp, from basic setup to your first API interactions. 72 |

73 | 74 |

Prerequisites

75 |

Before starting, ensure you have:

76 |
    77 |
  • Built the openapi-mcp binaries (see Installation)
  • 78 |
  • An OpenAPI 3.x YAML or JSON specification file
  • 79 |
80 | 81 |
82 |

Sample OpenAPI Spec

83 |

84 | If you don't have an OpenAPI spec handy, you can use the included example: 85 |

86 |
examples/fastly-openapi-mcp.yaml
87 |
88 | 89 |

Step 1: Start the MCP Server

90 |

91 | The simplest way to run openapi-mcp is in stdio mode with your OpenAPI spec: 92 |

93 |
bin/openapi-mcp examples/fastly-openapi-mcp.yaml
94 | 95 |

96 | This loads the OpenAPI spec, validates it, generates MCP tools for each operation, and starts an MCP server that communicates over stdin/stdout. 97 |

98 | 99 |

100 | If your API requires authentication, you can provide it via command-line flags or environment variables: 101 |

102 |
# Using API key
103 | API_KEY=your_api_key bin/openapi-mcp examples/fastly-openapi-mcp.yaml
104 | 
105 | # Using Bearer token
106 | BEARER_TOKEN=your_token bin/openapi-mcp examples/fastly-openapi-mcp.yaml
107 | 108 |

109 | To run as an HTTP server instead of stdio mode: 110 |

111 |
bin/openapi-mcp --http=:8080 examples/fastly-openapi-mcp.yaml
112 | 113 |
114 |

HTTP Mode Authentication

115 |

116 | When using HTTP mode, you can provide authentication via HTTP headers in your requests: 117 |

118 |
# Example with API key header
119 | curl -H "X-API-Key: your_key" http://localhost:8080/mcp -d '...'
120 | 
121 | # Example with Bearer token  
122 | curl -H "Authorization: Bearer your_token" http://localhost:8080/mcp -d '...'
123 |
124 | 125 |

Step 2: Use the Interactive Client

126 |

127 | The mcp-client is an interactive client that allows you to explore the available MCP tools and make calls: 128 |

129 |
bin/mcp-client bin/openapi-mcp examples/fastly-openapi-mcp.yaml
130 | 131 |

132 | If you're running in HTTP mode, connect to the HTTP endpoint: 133 |

134 |
bin/mcp-client http://localhost:8080
135 | 136 |

Step 3: Explore Available Tools

137 |

138 | Once in the client, you can list all available tools: 139 |

140 |
mcp> list
141 | 142 |

143 | This will display all the operations from your OpenAPI spec that have been converted to MCP tools. 144 |

145 | 146 |

Step 4: View Tool Schema

147 |

148 | To see the schema for a specific tool, use the schema command: 149 |

150 |
mcp> schema createService
151 | 152 |

153 | This shows the expected input parameters, including data types, descriptions, and whether they're required. 154 |

155 | 156 |

Step 5: Call a Tool

157 |

158 | To call a tool, use the call command with JSON arguments: 159 |

160 |
mcp> call createService {"name": "My Service", "type": "vcl"}
161 | 162 |

163 | The client will make the API call and display the response. 164 |

165 | 166 |

Step 6: Handle Confirmation for Dangerous Operations

167 |

168 | For dangerous operations (PUT, POST, DELETE), openapi-mcp requires confirmation: 169 |

170 |
mcp> call deleteService {"service_id": "SU1Z0isxPaozGVKXdv0eY"}
171 | 
172 | {
173 |   "type": "confirmation_request",
174 |   "confirmation_required": true,
175 |   "message": "This action is irreversible. Proceed?",
176 |   "action": "delete_resource"
177 | }
178 | 179 |

180 | To proceed, retry with the __confirmed parameter: 181 |

182 |
mcp> call deleteService {"service_id": "SU1Z0isxPaozGVKXdv0eY", "__confirmed": true}
183 | 184 |

Step 7: Get Complete API Documentation

185 |

186 | To get complete documentation for all available tools, use the describe tool: 187 |

188 |
mcp> call describe
189 | 190 |

191 | This returns comprehensive information about all available tools, including schemas, examples, and descriptions. 192 |

193 | 194 |

Serving Multiple APIs (Multi-Mount)

195 |

196 | You can serve multiple OpenAPI specs at different base paths using the --mount flag in HTTP mode: 197 |

198 |
openapi-mcp --http=:8080 --mount /petstore:petstore.yaml --mount /books:books.yaml
199 |         
200 |

201 | By default, this will serve the Petstore API at /petstore (StreamableHTTP), and the Books API at /books. If you use --http-transport=sse, endpoints like /petstore/sse and /petstore/message will be available for SSE clients. 202 |

203 | 204 |

Next Steps

205 |

206 | Now that you've got the basics down, here are some next steps: 207 |

208 | 214 |
215 |
216 |
217 | 218 | 219 |
220 |
221 | 246 | 247 | 250 |
251 |
252 | 253 | 254 | 255 | 256 | 257 | -------------------------------------------------------------------------------- /docs/docs/safety-features.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Safety Features - openapi-mcp Documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 44 | 45 | 46 |
47 |
48 | 49 | 65 | 66 | 67 |
68 |

Safety Features

69 | 70 |

71 | openapi-mcp includes several safety features to protect against unintended API changes. These features are especially important when using openapi-mcp with AI agents, which might otherwise make potentially destructive API calls without explicit user confirmation. 72 |

73 | 74 |

Confirmation for Dangerous Operations

75 |

76 | By default, openapi-mcp requires confirmation for any operations that use HTTP methods that can modify resources: 77 |

78 |
    79 |
  • PUT - Updates resources
  • 80 |
  • POST - Creates resources
  • 81 |
  • DELETE - Removes resources
  • 82 |
83 | 84 |
85 |

Confirmation Workflow

86 |

87 | When a client attempts to call a tool that uses one of these methods, openapi-mcp returns a confirmation request instead of immediately executing the operation: 88 |

89 | 90 |
// Request
 91 | {
 92 |   "jsonrpc": "2.0",
 93 |   "id": 1,
 94 |   "method": "tools/call",
 95 |   "params": {
 96 |     "name": "deleteService",
 97 |     "arguments": {
 98 |       "service_id": "SU1Z0isxPaozGVKXdv0eY"
 99 |     }
100 |   }
101 | }
102 | 
103 | // Response
104 | {
105 |   "jsonrpc": "2.0",
106 |   "id": 1,
107 |   "result": {
108 |     "OutputFormat": "structured",
109 |     "OutputType": "json",
110 |     "type": "confirmation_request",
111 |     "confirmation_required": true,
112 |     "message": "This action is irreversible. Proceed?",
113 |     "action": "delete_resource"
114 |   }
115 | }
116 | 117 |

118 | To proceed with the operation, the client must retry the call with the __confirmed parameter set to true: 119 |

120 | 121 |
// Request with confirmation
122 | {
123 |   "jsonrpc": "2.0",
124 |   "id": 2,
125 |   "method": "tools/call",
126 |   "params": {
127 |     "name": "deleteService",
128 |     "arguments": {
129 |       "service_id": "SU1Z0isxPaozGVKXdv0eY",
130 |       "__confirmed": true
131 |     }
132 |   }
133 | }
134 | 
135 | // Success response
136 | {
137 |   "jsonrpc": "2.0",
138 |   "id": 2,
139 |   "result": {
140 |     "OutputFormat": "structured",
141 |     "OutputType": "json",
142 |     "type": "api_response",
143 |     "data": {
144 |       "status": "ok"
145 |     },
146 |     "metadata": {
147 |       "status_code": 200
148 |     }
149 |   }
150 | }
151 |
152 | 153 |

Interactive Client Confirmation

154 |

155 | When using the interactive client (mcp-client), you'll see the confirmation request and be prompted to retry with confirmation: 156 |

157 | 158 |
mcp> call deleteService {"service_id": "SU1Z0isxPaozGVKXdv0eY"}
159 | 
160 | Confirmation required: This action is irreversible. Proceed?
161 | To confirm, retry with: call deleteService {"service_id": "SU1Z0isxPaozGVKXdv0eY", "__confirmed": true}
162 | 
163 | mcp> call deleteService {"service_id": "SU1Z0isxPaozGVKXdv0eY", "__confirmed": true}
164 | 
165 | HTTP DELETE /service/SU1Z0isxPaozGVKXdv0eY
166 | Status: 200
167 | Response:
168 | {
169 |   "status": "ok"
170 | }
171 | 172 |

AI Agent Confirmation Handling

173 |

174 | When integrating with AI agents, it's important to implement proper confirmation handling. Agents should: 175 |

176 |
    177 |
  • Detect the confirmation_request response type
  • 178 |
  • Present the confirmation message to the user
  • 179 |
  • Only retry with __confirmed: true if the user explicitly approves
  • 180 |
181 | 182 |
183 |

Example Agent Confirmation Code

184 |
async function callTool(name, args) {
185 |   const response = await sendMcpRequest({
186 |     jsonrpc: '2.0',
187 |     id: 1,
188 |     method: 'tools/call',
189 |     params: { name, arguments: args }
190 |   });
191 |   
192 |   // Check for confirmation request
193 |   if (response.result && response.result.type === 'confirmation_request') {
194 |     // Present confirmation to user
195 |     const userConfirmed = await askUserForConfirmation(response.result.message);
196 |     
197 |     if (userConfirmed) {
198 |       // Retry with confirmation
199 |       return callTool(name, { ...args, __confirmed: true });
200 |     } else {
201 |       throw new Error('Operation cancelled by user');
202 |     }
203 |   }
204 |   
205 |   return response.result;
206 | }
207 |
208 | 209 |

Disabling Confirmation

210 |

211 | While not recommended for most use cases, you can disable the confirmation requirement using the --no-confirm-dangerous flag: 212 |

213 | 214 |
bin/openapi-mcp --no-confirm-dangerous examples/fastly-openapi-mcp.yaml
215 | 216 |
217 |

Warning

218 |

219 | Disabling confirmation can lead to unintended modifications. Use with caution, especially in production environments or when used with AI agents. This should only be used in controlled automation scenarios where the risk is well understood. 220 |

221 |
222 | 223 |

Schema Validation

224 |

225 | In addition to the confirmation workflow, openapi-mcp includes comprehensive schema validation to prevent malformed requests: 226 |

227 |
    228 |
  • All tool call arguments are validated against the OpenAPI schema
  • 229 |
  • Required parameters are enforced
  • 230 |
  • Type checking ensures values match expected types
  • 231 |
  • Enum validation ensures values are within allowed options
  • 232 |
233 | 234 |

235 | When validation fails, a structured error response is returned with details about what went wrong: 236 |

237 | 238 |
{
239 |   "OutputFormat": "structured",
240 |   "OutputType": "json",
241 |   "type": "error",
242 |   "error": {
243 |     "code": "validation_error",
244 |     "message": "Invalid parameter",
245 |     "details": {
246 |       "field": "name",
247 |       "reason": "required field missing"
248 |     },
249 |     "suggestions": [
250 |       "Provide a name parameter"
251 |     ]
252 |   }
253 | }
254 | 255 |

Next Steps

256 |

257 | Now that you understand the safety features, you can: 258 |

259 | 264 |
265 |
266 |
267 | 268 | 269 |
270 |
271 | 296 | 297 | 300 |
301 |
302 | 303 | 304 | 305 | 306 | 307 | -------------------------------------------------------------------------------- /docs/images/favicon.ico: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/js/main.js: -------------------------------------------------------------------------------- 1 | // Main JavaScript file for openapi-mcp website 2 | 3 | document.addEventListener('DOMContentLoaded', function() { 4 | // Mobile menu toggle functionality 5 | const mobileMenuToggle = document.querySelector('.mobile-menu-toggle'); 6 | const navbar = document.querySelector('.navbar'); 7 | 8 | if (mobileMenuToggle) { 9 | mobileMenuToggle.addEventListener('click', function() { 10 | document.body.classList.toggle('mobile-nav-open'); 11 | }); 12 | } 13 | 14 | // Add active class to current nav link 15 | const currentPath = window.location.pathname; 16 | const navLinks = document.querySelectorAll('.nav-links a'); 17 | 18 | navLinks.forEach(link => { 19 | const linkPath = link.getAttribute('href'); 20 | if (currentPath === linkPath || 21 | (linkPath !== '/' && currentPath.startsWith(linkPath))) { 22 | link.classList.add('active'); 23 | } 24 | }); 25 | 26 | // Initialize code highlighting if highlight.js is loaded 27 | if (typeof hljs !== 'undefined') { 28 | document.querySelectorAll('pre code').forEach((block) => { 29 | hljs.highlightBlock(block); 30 | }); 31 | } 32 | 33 | // Smooth scrolling for anchor links 34 | document.querySelectorAll('a[href^="#"]').forEach(anchor => { 35 | anchor.addEventListener('click', function (e) { 36 | e.preventDefault(); 37 | 38 | const targetId = this.getAttribute('href').substring(1); 39 | const targetElement = document.getElementById(targetId); 40 | 41 | if (targetElement) { 42 | window.scrollTo({ 43 | top: targetElement.offsetTop - 80, // Offset for fixed header 44 | behavior: 'smooth' 45 | }); 46 | } 47 | }); 48 | }); 49 | 50 | // Add copy functionality to code blocks 51 | document.querySelectorAll('pre').forEach(block => { 52 | const copyButton = document.createElement('button'); 53 | copyButton.className = 'copy-button'; 54 | copyButton.innerHTML = ''; 55 | copyButton.style.position = 'absolute'; 56 | copyButton.style.top = '0.5rem'; 57 | copyButton.style.right = '0.5rem'; 58 | copyButton.style.padding = '0.25rem'; 59 | copyButton.style.background = 'rgba(0, 0, 0, 0.3)'; 60 | copyButton.style.border = 'none'; 61 | copyButton.style.borderRadius = '4px'; 62 | copyButton.style.color = 'white'; 63 | copyButton.style.cursor = 'pointer'; 64 | 65 | // Make the pre position relative to position the button 66 | block.style.position = 'relative'; 67 | 68 | block.appendChild(copyButton); 69 | 70 | copyButton.addEventListener('click', () => { 71 | const code = block.querySelector('code') || block; 72 | const text = code.innerText; 73 | 74 | navigator.clipboard.writeText(text).then(() => { 75 | copyButton.innerHTML = ''; 76 | 77 | setTimeout(() => { 78 | copyButton.innerHTML = ''; 79 | }, 2000); 80 | }).catch(err => { 81 | console.error('Failed to copy text: ', err); 82 | }); 83 | }); 84 | }); 85 | 86 | // Handle documentation sidebar (if exists) 87 | const docsSidebar = document.querySelector('.docs-sidebar'); 88 | if (docsSidebar) { 89 | const docsLinks = docsSidebar.querySelectorAll('a'); 90 | const currentDocPath = window.location.pathname; 91 | 92 | docsLinks.forEach(link => { 93 | const linkPath = link.getAttribute('href'); 94 | if (currentDocPath === linkPath) { 95 | link.classList.add('active'); 96 | 97 | // Expand parent sections if any 98 | const parentLi = link.closest('li.has-submenu'); 99 | if (parentLi) { 100 | parentLi.classList.add('expanded'); 101 | } 102 | } 103 | }); 104 | 105 | // Toggle submenu items 106 | const submenuToggles = docsSidebar.querySelectorAll('.submenu-toggle'); 107 | submenuToggles.forEach(toggle => { 108 | toggle.addEventListener('click', function(e) { 109 | e.preventDefault(); 110 | const li = this.closest('li'); 111 | li.classList.toggle('expanded'); 112 | }); 113 | }); 114 | } 115 | }); -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jedisct1/openapi-mcp 2 | 3 | go 1.22.5 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | github.com/chzyer/readline v1.5.1 9 | github.com/getkin/kin-openapi v0.132.0 10 | github.com/google/uuid v1.6.0 11 | github.com/spf13/cast v1.8.0 12 | github.com/xeipuuv/gojsonschema v1.2.0 13 | github.com/yosida95/uritemplate/v3 v3.0.2 14 | ) 15 | 16 | require ( 17 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 18 | github.com/go-openapi/swag v0.23.0 // indirect 19 | github.com/josharian/intern v1.0.0 // indirect 20 | github.com/mailru/easyjson v0.7.7 // indirect 21 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 22 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect 23 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect 24 | github.com/perimeterx/marshmallow v1.1.5 // indirect 25 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect 26 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 27 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect 28 | gopkg.in/yaml.v3 v3.0.1 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= 2 | github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= 3 | github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= 4 | github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= 5 | github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= 6 | github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 11 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 12 | github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk= 13 | github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= 14 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 15 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 16 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 17 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 18 | github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= 19 | github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 20 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 21 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 22 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 23 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 24 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 25 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 26 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 27 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 28 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 29 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 30 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 31 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 32 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= 33 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= 34 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= 35 | github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= 36 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= 37 | github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= 38 | github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= 39 | github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= 40 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 41 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 42 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 43 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 44 | github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk= 45 | github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 46 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 47 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 48 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 49 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 50 | github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= 51 | github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= 52 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= 53 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 54 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= 55 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 56 | github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= 57 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 58 | github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= 59 | github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= 60 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng= 61 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 62 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 63 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 64 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 65 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 66 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 67 | -------------------------------------------------------------------------------- /openapi-validator/README.md: -------------------------------------------------------------------------------- 1 | # OpenAPI Validator Web Interface 2 | 3 | A modern, responsive web interface for validating and linting OpenAPI specifications using the `openapi-mcp` HTTP API. 4 | 5 | ## Features 6 | 7 | - **Drag & Drop File Upload**: Simply drag your OpenAPI file onto the upload area 8 | - **Text Input**: Paste your OpenAPI specification directly into the text editor 9 | - **Real-time Validation**: Validate against remote `openapi-mcp` servers 10 | - **Comprehensive Linting**: Choose between basic validation or detailed linting 11 | - **Detailed Results**: View errors and warnings with actionable suggestions 12 | - **Export Results**: Download validation results as JSON 13 | - **Responsive Design**: Works great on desktop and mobile devices 14 | 15 | ## Getting Started 16 | 17 | ### 1. Start the OpenAPI Validation Server 18 | 19 | First, ensure you have the `openapi-mcp` tool running in HTTP mode: 20 | 21 | ```bash 22 | # Start the HTTP server (both validate and lint endpoints will be available) 23 | ./openapi-mcp --http=:8080 validate 24 | # OR 25 | ./openapi-mcp --http=:8080 lint 26 | 27 | # Both commands now expose both /validate and /lint endpoints 28 | # The web interface can switch between modes dynamically 29 | ``` 30 | 31 | ### 2. Open the Web Interface 32 | 33 | Simply open `index.html` in your web browser. You can: 34 | 35 | - Open it directly from your file system 36 | - Serve it with a local web server for better performance: 37 | 38 | ```bash 39 | # Using Python 40 | python -m http.server 8000 41 | 42 | # Using Node.js (if you have http-server installed) 43 | npx http-server 44 | 45 | # Using PHP 46 | php -S localhost:8000 47 | ``` 48 | 49 | ### 3. Configure the Server URL 50 | 51 | In the web interface: 52 | 53 | 1. Set the **Validation Server URL** to match your running server (default: `http://localhost:8080`) 54 | 2. Choose your **Validation Mode**: 55 | - **Validate**: Basic validation for critical issues 56 | - **Lint**: Comprehensive analysis with best practice suggestions 57 | 3. Click **Test Connection** to verify connectivity 58 | 59 | ### 4. Validate Your OpenAPI Spec 60 | 61 | You can provide your OpenAPI specification in two ways: 62 | 63 | #### Option A: File Upload 64 | - Drag and drop your `.yaml`, `.yml`, or `.json` file onto the upload area 65 | - Or click the upload area to browse for files 66 | 67 | #### Option B: Text Input 68 | - Paste your OpenAPI specification directly into the text editor 69 | - Click **Load Example** to see a sample specification 70 | 71 | Then click **Validate** or **Lint** to analyze your specification. 72 | 73 | ## Understanding Results 74 | 75 | ### Success 76 | When validation passes, you'll see a green success message indicating your OpenAPI specification is valid. 77 | 78 | ### Issues 79 | When issues are found, they're categorized as: 80 | 81 | - **Errors** (🔴): Critical issues that prevent the specification from working 82 | - **Warnings** (🟡): Best practice violations or potential improvements 83 | 84 | Each issue includes: 85 | - **Message**: Description of the problem 86 | - **Suggestion**: Actionable advice for fixing the issue 87 | - **Context**: Location information (operation, path, method, etc.) 88 | 89 | ### Export Results 90 | Click **Export Results** to download the full validation report as a JSON file for further analysis or documentation. 91 | 92 | ## Server Configuration 93 | 94 | The web interface can connect to any `openapi-mcp` server running with HTTP API enabled. Common configurations: 95 | 96 | ### Local Development 97 | ```bash 98 | # Basic validation server 99 | ./openapi-mcp --http=:8080 validate 100 | 101 | # Comprehensive linting server 102 | ./openapi-mcp --http=:8080 lint 103 | ``` 104 | 105 | ### Remote Server 106 | Update the server URL in the web interface to point to your remote validation service: 107 | ``` 108 | https://your-domain.com:8080 109 | ``` 110 | 111 | ### Custom Port 112 | ```bash 113 | # Run on a different port 114 | ./openapi-mcp --http=:3000 validate 115 | ``` 116 | 117 | Then update the server URL to `http://localhost:3000`. 118 | 119 | ## Example OpenAPI Specifications 120 | 121 | The interface includes a **Load Example** button that provides a sample Pet Store API specification. This is useful for: 122 | 123 | - Testing the validation functionality 124 | - Learning OpenAPI best practices 125 | - Understanding the types of issues the linter can detect 126 | 127 | ## Browser Compatibility 128 | 129 | This web interface works with all modern browsers that support: 130 | - ES6+ JavaScript features 131 | - CSS Grid and Flexbox 132 | - Fetch API 133 | - File API (for drag & drop) 134 | 135 | Tested browsers: 136 | - Chrome 90+ 137 | - Firefox 88+ 138 | - Safari 14+ 139 | - Edge 90+ 140 | 141 | ## CORS Support 142 | 143 | The `openapi-mcp` HTTP API includes full CORS support, allowing the web interface to connect from any origin. This means you can: 144 | 145 | - Host the web interface on any domain 146 | - Use it locally without CORS issues 147 | - Deploy it to static hosting services like GitHub Pages, Netlify, etc. 148 | 149 | ## Deployment 150 | 151 | To deploy this web interface: 152 | 153 | 1. **Static Hosting**: Upload the files to any static web hosting service 154 | 2. **GitHub Pages**: Push to a GitHub repository and enable Pages 155 | 3. **Netlify/Vercel**: Connect your repository for automatic deployments 156 | 4. **CDN**: Use any CDN service to serve the static files 157 | 158 | No server-side configuration is needed - this is a pure client-side application. 159 | 160 | ## Troubleshooting 161 | 162 | ### Connection Issues 163 | - Ensure the `openapi-mcp` server is running 164 | - Check the server URL and port 165 | - Verify firewall settings if using remote servers 166 | - Use the **Test Connection** button to diagnose connectivity 167 | 168 | ### Validation Failures 169 | - Check that your OpenAPI specification is valid YAML or JSON 170 | - Ensure it follows OpenAPI 3.x format 171 | - Review the detailed error messages for specific issues 172 | 173 | ### Browser Console 174 | Open your browser's developer tools and check the console for any JavaScript errors or network issues. 175 | 176 | ## Contributing 177 | 178 | To improve this web interface: 179 | 180 | 1. Modify the HTML structure in `index.html` 181 | 2. Update styling in `styles.css` 182 | 3. Enhance functionality in `script.js` 183 | 4. Test with various OpenAPI specifications 184 | 5. Ensure responsive design works across devices 185 | 186 | The code is well-commented and follows modern web development practices. -------------------------------------------------------------------------------- /openapi-validator/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | OpenAPI Validator 7 | 8 | 9 | 10 | 11 |
12 |
13 |

🔍 OpenAPI Validator

14 |

Validate and lint your OpenAPI specifications with detailed feedback

15 |
16 | 17 |
18 | 19 |
20 |

Configuration

21 |
22 |
23 | 24 | 26 |
27 |
28 | 29 | 33 |
34 |
35 | 36 |
37 |
38 | 39 | 40 |
41 |

Upload OpenAPI Specification

42 |
43 |
44 |
📄
45 |

Drag and drop your OpenAPI file here

46 |

or click to browse

47 | 48 |
49 |
50 | 57 |
58 | 59 | 60 |
61 |

Or Paste OpenAPI Specification

62 | 63 |
64 | 65 | 66 |
67 |
68 | 69 | 70 |
71 | 75 |
76 | 77 | 78 | 88 |
89 | 90 | 93 |
94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /openapi-validator/styles.css: -------------------------------------------------------------------------------- 1 | /* Reset and base styles */ 2 | * { 3 | margin: 0; 4 | padding: 0; 5 | box-sizing: border-box; 6 | } 7 | 8 | body { 9 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; 10 | line-height: 1.6; 11 | color: #333; 12 | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 13 | min-height: 100vh; 14 | } 15 | 16 | .container { 17 | max-width: 1200px; 18 | margin: 0 auto; 19 | padding: 20px; 20 | } 21 | 22 | /* Header */ 23 | header { 24 | text-align: center; 25 | margin-bottom: 2rem; 26 | background: rgba(255, 255, 255, 0.95); 27 | padding: 2rem; 28 | border-radius: 12px; 29 | backdrop-filter: blur(10px); 30 | box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); 31 | } 32 | 33 | header h1 { 34 | font-size: 2.5rem; 35 | margin-bottom: 0.5rem; 36 | background: linear-gradient(135deg, #667eea, #764ba2); 37 | -webkit-background-clip: text; 38 | -webkit-text-fill-color: transparent; 39 | background-clip: text; 40 | } 41 | 42 | header p { 43 | font-size: 1.1rem; 44 | color: #666; 45 | } 46 | 47 | /* Main content */ 48 | main { 49 | display: flex; 50 | flex-direction: column; 51 | gap: 2rem; 52 | } 53 | 54 | /* Sections */ 55 | section { 56 | background: rgba(255, 255, 255, 0.95); 57 | padding: 2rem; 58 | border-radius: 12px; 59 | backdrop-filter: blur(10px); 60 | box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); 61 | } 62 | 63 | section h2 { 64 | margin-bottom: 1.5rem; 65 | color: #333; 66 | font-size: 1.4rem; 67 | } 68 | 69 | /* Configuration Section */ 70 | .config-grid { 71 | display: grid; 72 | grid-template-columns: 1fr 1fr; 73 | gap: 1rem; 74 | margin-bottom: 1rem; 75 | } 76 | 77 | .config-item { 78 | display: flex; 79 | flex-direction: column; 80 | gap: 0.5rem; 81 | } 82 | 83 | .config-item label { 84 | font-weight: 600; 85 | color: #555; 86 | } 87 | 88 | .config-item input, 89 | .config-item select { 90 | padding: 0.75rem; 91 | border: 2px solid #e1e5e9; 92 | border-radius: 8px; 93 | font-size: 0.95rem; 94 | transition: border-color 0.2s; 95 | } 96 | 97 | .config-item input:focus, 98 | .config-item select:focus { 99 | outline: none; 100 | border-color: #667eea; 101 | } 102 | 103 | /* Status indicator */ 104 | .status-indicator { 105 | margin-top: 1rem; 106 | padding: 0.75rem; 107 | border-radius: 8px; 108 | font-weight: 500; 109 | text-align: center; 110 | display: none; 111 | } 112 | 113 | .status-indicator.success { 114 | background: #d4edda; 115 | color: #155724; 116 | border: 1px solid #c3e6cb; 117 | display: block; 118 | } 119 | 120 | .status-indicator.error { 121 | background: #f8d7da; 122 | color: #721c24; 123 | border: 1px solid #f5c6cb; 124 | display: block; 125 | } 126 | 127 | .status-indicator.testing { 128 | background: #fff3cd; 129 | color: #856404; 130 | border: 1px solid #ffeaa7; 131 | display: block; 132 | } 133 | 134 | /* Upload Section */ 135 | .upload-area { 136 | border: 3px dashed #ddd; 137 | border-radius: 12px; 138 | padding: 3rem; 139 | text-align: center; 140 | cursor: pointer; 141 | transition: all 0.3s ease; 142 | background: rgba(255, 255, 255, 0.5); 143 | } 144 | 145 | .upload-area:hover, 146 | .upload-area.dragover { 147 | border-color: #667eea; 148 | background: rgba(102, 126, 234, 0.05); 149 | } 150 | 151 | .upload-content { 152 | pointer-events: none; 153 | } 154 | 155 | .upload-icon { 156 | font-size: 3rem; 157 | margin-bottom: 1rem; 158 | } 159 | 160 | .upload-area p { 161 | font-size: 1.1rem; 162 | margin-bottom: 0.5rem; 163 | } 164 | 165 | .upload-subtitle { 166 | color: #666; 167 | font-size: 0.9rem !important; 168 | } 169 | 170 | .file-info { 171 | display: flex; 172 | justify-content: space-between; 173 | align-items: center; 174 | padding: 1rem; 175 | background: rgba(102, 126, 234, 0.1); 176 | border-radius: 8px; 177 | margin-top: 1rem; 178 | } 179 | 180 | .file-details { 181 | display: flex; 182 | flex-direction: column; 183 | gap: 0.25rem; 184 | } 185 | 186 | .file-name { 187 | font-weight: 600; 188 | color: #333; 189 | } 190 | 191 | .file-size { 192 | font-size: 0.9rem; 193 | color: #666; 194 | } 195 | 196 | /* Text Input Section */ 197 | #spec-input { 198 | width: 100%; 199 | height: 300px; 200 | padding: 1rem; 201 | border: 2px solid #e1e5e9; 202 | border-radius: 8px; 203 | font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; 204 | font-size: 0.9rem; 205 | resize: vertical; 206 | transition: border-color 0.2s; 207 | } 208 | 209 | #spec-input:focus { 210 | outline: none; 211 | border-color: #667eea; 212 | } 213 | 214 | .text-actions { 215 | display: flex; 216 | gap: 1rem; 217 | margin-top: 1rem; 218 | justify-content: flex-end; 219 | } 220 | 221 | /* Actions Section */ 222 | .actions-section { 223 | text-align: center; 224 | } 225 | 226 | /* Buttons */ 227 | .btn { 228 | padding: 0.75rem 1.5rem; 229 | border: none; 230 | border-radius: 8px; 231 | font-size: 1rem; 232 | font-weight: 600; 233 | cursor: pointer; 234 | transition: all 0.2s; 235 | display: inline-flex; 236 | align-items: center; 237 | gap: 0.5rem; 238 | text-decoration: none; 239 | } 240 | 241 | .btn:disabled { 242 | background: #e9ecef !important; 243 | color: #6c757d !important; 244 | cursor: not-allowed; 245 | opacity: 1; 246 | } 247 | 248 | .btn-primary { 249 | background: linear-gradient(135deg, #4c63d2, #5a4fcf) !important; 250 | color: white !important; 251 | padding: 1rem 2rem; 252 | font-size: 1.1rem; 253 | font-weight: 700; 254 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 255 | text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) !important; 256 | } 257 | 258 | .btn-primary:not(:disabled), 259 | .btn-primary:active, 260 | .btn-primary:focus, 261 | .btn-primary:visited { 262 | color: white !important; 263 | text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) !important; 264 | } 265 | 266 | .btn-primary * { 267 | color: white !important; 268 | text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) !important; 269 | } 270 | 271 | .btn-primary .btn-text, 272 | .btn-primary .btn-spinner { 273 | color: white !important; 274 | background: transparent !important; 275 | text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) !important; 276 | } 277 | 278 | .btn-primary:hover:not(:disabled) { 279 | transform: translateY(-2px); 280 | box-shadow: 0 4px 12px rgba(76, 99, 210, 0.4); 281 | background: linear-gradient(135deg, #3b4bc7, #4a3fcc) !important; 282 | color: white !important; 283 | } 284 | 285 | .btn-primary:hover:not(:disabled) * { 286 | color: white !important; 287 | } 288 | 289 | .btn-primary:hover:not(:disabled) .btn-text, 290 | .btn-primary:hover:not(:disabled) .btn-spinner { 291 | color: white !important; 292 | background: transparent !important; 293 | } 294 | 295 | .btn-secondary { 296 | background: #f8f9fa; 297 | color: #333; 298 | border: 2px solid #e1e5e9; 299 | } 300 | 301 | .btn-secondary:hover { 302 | background: #e9ecef; 303 | border-color: #adb5bd; 304 | } 305 | 306 | .btn-text { 307 | background: transparent; 308 | color: #667eea; 309 | padding: 0.5rem 1rem; 310 | } 311 | 312 | .btn-text:hover { 313 | background: rgba(102, 126, 234, 0.1); 314 | } 315 | 316 | .btn-spinner { 317 | animation: spin 1s linear infinite; 318 | } 319 | 320 | @keyframes spin { 321 | from { transform: rotate(0deg); } 322 | to { transform: rotate(360deg); } 323 | } 324 | 325 | /* Results Section */ 326 | .results-header { 327 | display: flex; 328 | justify-content: space-between; 329 | align-items: center; 330 | margin-bottom: 1.5rem; 331 | flex-wrap: wrap; 332 | gap: 1rem; 333 | } 334 | 335 | .results-summary { 336 | display: flex; 337 | gap: 1rem; 338 | flex-wrap: wrap; 339 | } 340 | 341 | .summary-item { 342 | padding: 0.5rem 1rem; 343 | border-radius: 20px; 344 | font-weight: 600; 345 | font-size: 0.9rem; 346 | } 347 | 348 | .summary-success { 349 | background: #d4edda; 350 | color: #155724; 351 | } 352 | 353 | .summary-error { 354 | background: #f8d7da; 355 | color: #721c24; 356 | } 357 | 358 | .summary-warning { 359 | background: #fff3cd; 360 | color: #856404; 361 | } 362 | 363 | /* Issues List */ 364 | .issues-list { 365 | display: flex; 366 | flex-direction: column; 367 | gap: 1rem; 368 | } 369 | 370 | .issue-item { 371 | padding: 1rem; 372 | border-radius: 8px; 373 | border-left: 4px solid; 374 | } 375 | 376 | .issue-error { 377 | background: #f8d7da; 378 | border-left-color: #dc3545; 379 | } 380 | 381 | .issue-warning { 382 | background: #fff3cd; 383 | border-left-color: #ffc107; 384 | } 385 | 386 | .issue-header { 387 | display: flex; 388 | justify-content: between; 389 | align-items: flex-start; 390 | margin-bottom: 0.5rem; 391 | gap: 1rem; 392 | } 393 | 394 | .issue-type { 395 | padding: 0.25rem 0.5rem; 396 | border-radius: 4px; 397 | font-size: 0.8rem; 398 | font-weight: 600; 399 | text-transform: uppercase; 400 | flex-shrink: 0; 401 | } 402 | 403 | .issue-type.error { 404 | background: #dc3545; 405 | color: white; 406 | } 407 | 408 | .issue-type.warning { 409 | background: #ffc107; 410 | color: #212529; 411 | } 412 | 413 | .issue-message { 414 | font-weight: 600; 415 | margin-bottom: 0.5rem; 416 | flex-grow: 1; 417 | } 418 | 419 | .issue-suggestion { 420 | color: #666; 421 | font-style: italic; 422 | margin-bottom: 0.5rem; 423 | } 424 | 425 | .issue-context { 426 | display: flex; 427 | gap: 1rem; 428 | font-size: 0.9rem; 429 | color: #555; 430 | flex-wrap: wrap; 431 | } 432 | 433 | .issue-context span { 434 | background: rgba(0, 0, 0, 0.05); 435 | padding: 0.25rem 0.5rem; 436 | border-radius: 4px; 437 | } 438 | 439 | /* Success message */ 440 | .success-message { 441 | text-align: center; 442 | padding: 2rem; 443 | color: #155724; 444 | } 445 | 446 | .success-message .success-icon { 447 | font-size: 3rem; 448 | margin-bottom: 1rem; 449 | } 450 | 451 | /* Footer */ 452 | footer { 453 | text-align: center; 454 | margin-top: 2rem; 455 | padding: 1rem; 456 | color: rgba(255, 255, 255, 0.8); 457 | } 458 | 459 | footer a { 460 | color: rgba(255, 255, 255, 0.9); 461 | text-decoration: none; 462 | } 463 | 464 | footer a:hover { 465 | text-decoration: underline; 466 | } 467 | 468 | /* Responsive design */ 469 | @media (max-width: 768px) { 470 | .container { 471 | padding: 10px; 472 | } 473 | 474 | .config-grid { 475 | grid-template-columns: 1fr; 476 | } 477 | 478 | .results-header { 479 | flex-direction: column; 480 | align-items: stretch; 481 | } 482 | 483 | .results-summary { 484 | justify-content: center; 485 | } 486 | 487 | .issue-header { 488 | flex-direction: column; 489 | gap: 0.5rem; 490 | } 491 | 492 | .issue-context { 493 | flex-direction: column; 494 | gap: 0.5rem; 495 | } 496 | 497 | header h1 { 498 | font-size: 2rem; 499 | } 500 | 501 | .upload-area { 502 | padding: 2rem 1rem; 503 | } 504 | } -------------------------------------------------------------------------------- /pkg/mcp/.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | exclusions: 4 | presets: 5 | - std-error-handling 6 | 7 | -------------------------------------------------------------------------------- /pkg/mcp/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | [contact@mark3labs.com](mailto:contact@mark3labs.com). 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /pkg/mcp/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in contributing to the MCP Go SDK! We welcome contributions of all kinds, including bug fixes, new features, and documentation improvements. This document outlines the process for contributing to the project. 4 | 5 | ## Development Guidelines 6 | 7 | ### Prerequisites 8 | 9 | Make sure you have Go 1.23 or later installed on your machine. You can check your Go version by running: 10 | 11 | ```bash 12 | go version 13 | ``` 14 | 15 | ### Setup 16 | 17 | 1. Fork the repository 18 | 2. Clone your fork: 19 | 20 | ```bash 21 | git clone https://github.com/YOUR_USERNAME/mcp-go.git 22 | cd mcp-go 23 | ``` 24 | 3. Install the required packages: 25 | 26 | ```bash 27 | go mod tidy 28 | ``` 29 | 30 | ### Workflow 31 | 32 | 1. Create a new branch. 33 | 2. Make your changes. 34 | 3. Ensure you have added tests for any new functionality. 35 | 4. Run the tests as shown below from the root directory: 36 | 37 | ```bash 38 | go test -v './...' 39 | ``` 40 | 5. Submit a pull request to the main branch. 41 | 42 | Feel free to reach out if you have any questions or need help either by [opening an issue](https://github.com/jedisct1/openapi-mcp/internal/mcp-go/issues) or by reaching out in the [Discord channel](https://discord.gg/RqSS2NQVsY). 43 | -------------------------------------------------------------------------------- /pkg/mcp/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Anthropic, PBC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pkg/mcp/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | Thank you for helping us improve the security of the project. Your contributions are greatly appreciated. 4 | 5 | ## Reporting a Vulnerability 6 | 7 | If you discover a security vulnerability within this project, please email the maintainers at [contact@mark3labs.com](mailto:contact@mark3labs.com). 8 | -------------------------------------------------------------------------------- /pkg/mcp/mcp/prompts.go: -------------------------------------------------------------------------------- 1 | package mcp 2 | 3 | /* Prompts */ 4 | 5 | // ListPromptsRequest is sent from the client to request a list of prompts and 6 | // prompt templates the server has. 7 | type ListPromptsRequest struct { 8 | PaginatedRequest 9 | } 10 | 11 | // ListPromptsResult is the server's response to a prompts/list request from 12 | // the client. 13 | type ListPromptsResult struct { 14 | PaginatedResult 15 | Prompts []Prompt `json:"prompts"` 16 | } 17 | 18 | // GetPromptRequest is used by the client to get a prompt provided by the 19 | // server. 20 | type GetPromptRequest struct { 21 | Request 22 | Params struct { 23 | // The name of the prompt or prompt template. 24 | Name string `json:"name"` 25 | // Arguments to use for templating the prompt. 26 | Arguments map[string]string `json:"arguments,omitempty"` 27 | } `json:"params"` 28 | } 29 | 30 | // GetPromptResult is the server's response to a prompts/get request from the 31 | // client. 32 | type GetPromptResult struct { 33 | Result 34 | // An optional description for the prompt. 35 | Description string `json:"description,omitempty"` 36 | Messages []PromptMessage `json:"messages"` 37 | } 38 | 39 | // Prompt represents a prompt or prompt template that the server offers. 40 | // If Arguments is non-nil and non-empty, this indicates the prompt is a template 41 | // that requires argument values to be provided when calling prompts/get. 42 | // If Arguments is nil or empty, this is a static prompt that takes no arguments. 43 | type Prompt struct { 44 | // The name of the prompt or prompt template. 45 | Name string `json:"name"` 46 | // An optional description of what this prompt provides 47 | Description string `json:"description,omitempty"` 48 | // A list of arguments to use for templating the prompt. 49 | // The presence of arguments indicates this is a template prompt. 50 | Arguments []PromptArgument `json:"arguments,omitempty"` 51 | } 52 | 53 | // GetName returns the name of the prompt. 54 | func (p Prompt) GetName() string { 55 | return p.Name 56 | } 57 | 58 | // PromptArgument describes an argument that a prompt template can accept. 59 | // When a prompt includes arguments, clients must provide values for all 60 | // required arguments when making a prompts/get request. 61 | type PromptArgument struct { 62 | // The name of the argument. 63 | Name string `json:"name"` 64 | // A human-readable description of the argument. 65 | Description string `json:"description,omitempty"` 66 | // Whether this argument must be provided. 67 | // If true, clients must include this argument when calling prompts/get. 68 | Required bool `json:"required,omitempty"` 69 | } 70 | 71 | // Role represents the sender or recipient of messages and data in a 72 | // conversation. 73 | type Role string 74 | 75 | const ( 76 | RoleUser Role = "user" 77 | RoleAssistant Role = "assistant" 78 | ) 79 | 80 | // PromptMessage describes a message returned as part of a prompt. 81 | // 82 | // This is similar to `SamplingMessage`, but also supports the embedding of 83 | // resources from the MCP server. 84 | type PromptMessage struct { 85 | Role Role `json:"role"` 86 | Content Content `json:"content"` // Can be TextContent, ImageContent, AudioContent or EmbeddedResource 87 | } 88 | 89 | // PromptListChangedNotification is an optional notification from the server 90 | // to the client, informing it that the list of prompts it offers has changed. This 91 | // may be issued by servers without any previous subscription from the client. 92 | type PromptListChangedNotification struct { 93 | Notification 94 | } 95 | 96 | // PromptOption is a function that configures a Prompt. 97 | // It provides a flexible way to set various properties of a Prompt using the functional options pattern. 98 | type PromptOption func(*Prompt) 99 | 100 | // ArgumentOption is a function that configures a PromptArgument. 101 | // It allows for flexible configuration of prompt arguments using the functional options pattern. 102 | type ArgumentOption func(*PromptArgument) 103 | 104 | // 105 | // Core Prompt Functions 106 | // 107 | 108 | // NewPrompt creates a new Prompt with the given name and options. 109 | // The prompt will be configured based on the provided options. 110 | // Options are applied in order, allowing for flexible prompt configuration. 111 | func NewPrompt(name string, opts ...PromptOption) Prompt { 112 | prompt := Prompt{ 113 | Name: name, 114 | } 115 | 116 | for _, opt := range opts { 117 | opt(&prompt) 118 | } 119 | 120 | return prompt 121 | } 122 | 123 | // WithPromptDescription adds a description to the Prompt. 124 | // The description should provide a clear, human-readable explanation of what the prompt does. 125 | func WithPromptDescription(description string) PromptOption { 126 | return func(p *Prompt) { 127 | p.Description = description 128 | } 129 | } 130 | 131 | // WithArgument adds an argument to the prompt's argument list. 132 | // The argument will be configured based on the provided options. 133 | func WithArgument(name string, opts ...ArgumentOption) PromptOption { 134 | return func(p *Prompt) { 135 | arg := PromptArgument{ 136 | Name: name, 137 | } 138 | 139 | for _, opt := range opts { 140 | opt(&arg) 141 | } 142 | 143 | if p.Arguments == nil { 144 | p.Arguments = make([]PromptArgument, 0) 145 | } 146 | p.Arguments = append(p.Arguments, arg) 147 | } 148 | } 149 | 150 | // 151 | // Argument Options 152 | // 153 | 154 | // ArgumentDescription adds a description to a prompt argument. 155 | // The description should explain the purpose and expected values of the argument. 156 | func ArgumentDescription(desc string) ArgumentOption { 157 | return func(arg *PromptArgument) { 158 | arg.Description = desc 159 | } 160 | } 161 | 162 | // RequiredArgument marks an argument as required in the prompt. 163 | // Required arguments must be provided when getting the prompt. 164 | func RequiredArgument() ArgumentOption { 165 | return func(arg *PromptArgument) { 166 | arg.Required = true 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /pkg/mcp/mcp/resources.go: -------------------------------------------------------------------------------- 1 | package mcp 2 | 3 | import "github.com/yosida95/uritemplate/v3" 4 | 5 | // ResourceOption is a function that configures a Resource. 6 | // It provides a flexible way to set various properties of a Resource using the functional options pattern. 7 | type ResourceOption func(*Resource) 8 | 9 | // NewResource creates a new Resource with the given URI, name and options. 10 | // The resource will be configured based on the provided options. 11 | // Options are applied in order, allowing for flexible resource configuration. 12 | func NewResource(uri string, name string, opts ...ResourceOption) Resource { 13 | resource := Resource{ 14 | URI: uri, 15 | Name: name, 16 | } 17 | 18 | for _, opt := range opts { 19 | opt(&resource) 20 | } 21 | 22 | return resource 23 | } 24 | 25 | // WithResourceDescription adds a description to the Resource. 26 | // The description should provide a clear, human-readable explanation of what the resource represents. 27 | func WithResourceDescription(description string) ResourceOption { 28 | return func(r *Resource) { 29 | r.Description = description 30 | } 31 | } 32 | 33 | // WithMIMEType sets the MIME type for the Resource. 34 | // This should indicate the format of the resource's contents. 35 | func WithMIMEType(mimeType string) ResourceOption { 36 | return func(r *Resource) { 37 | r.MIMEType = mimeType 38 | } 39 | } 40 | 41 | // WithAnnotations adds annotations to the Resource. 42 | // Annotations can provide additional metadata about the resource's intended use. 43 | func WithAnnotations(audience []Role, priority float64) ResourceOption { 44 | return func(r *Resource) { 45 | if r.Annotations == nil { 46 | r.Annotations = &Annotations{} 47 | } 48 | r.Annotations.Audience = audience 49 | r.Annotations.Priority = priority 50 | } 51 | } 52 | 53 | // ResourceTemplateOption is a function that configures a ResourceTemplate. 54 | // It provides a flexible way to set various properties of a ResourceTemplate using the functional options pattern. 55 | type ResourceTemplateOption func(*ResourceTemplate) 56 | 57 | // NewResourceTemplate creates a new ResourceTemplate with the given URI template, name and options. 58 | // The template will be configured based on the provided options. 59 | // Options are applied in order, allowing for flexible template configuration. 60 | func NewResourceTemplate(uriTemplate string, name string, opts ...ResourceTemplateOption) ResourceTemplate { 61 | template := ResourceTemplate{ 62 | URITemplate: &URITemplate{Template: uritemplate.MustNew(uriTemplate)}, 63 | Name: name, 64 | } 65 | 66 | for _, opt := range opts { 67 | opt(&template) 68 | } 69 | 70 | return template 71 | } 72 | 73 | // WithTemplateDescription adds a description to the ResourceTemplate. 74 | // The description should provide a clear, human-readable explanation of what resources this template represents. 75 | func WithTemplateDescription(description string) ResourceTemplateOption { 76 | return func(t *ResourceTemplate) { 77 | t.Description = description 78 | } 79 | } 80 | 81 | // WithTemplateMIMEType sets the MIME type for the ResourceTemplate. 82 | // This should only be set if all resources matching this template will have the same type. 83 | func WithTemplateMIMEType(mimeType string) ResourceTemplateOption { 84 | return func(t *ResourceTemplate) { 85 | t.MIMEType = mimeType 86 | } 87 | } 88 | 89 | // WithTemplateAnnotations adds annotations to the ResourceTemplate. 90 | // Annotations can provide additional metadata about the template's intended use. 91 | func WithTemplateAnnotations(audience []Role, priority float64) ResourceTemplateOption { 92 | return func(t *ResourceTemplate) { 93 | if t.Annotations == nil { 94 | t.Annotations = &Annotations{} 95 | } 96 | t.Annotations.Audience = audience 97 | t.Annotations.Priority = priority 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /pkg/mcp/server/errors.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | var ( 9 | // Common server errors 10 | ErrUnsupported = errors.New("not supported") 11 | ErrResourceNotFound = errors.New("resource not found") 12 | ErrPromptNotFound = errors.New("prompt not found") 13 | ErrToolNotFound = errors.New("tool not found") 14 | 15 | // Session-related errors 16 | ErrSessionNotFound = errors.New("session not found") 17 | ErrSessionExists = errors.New("session already exists") 18 | ErrSessionNotInitialized = errors.New("session not properly initialized") 19 | ErrSessionDoesNotSupportTools = errors.New("session does not support per-session tools") 20 | ErrSessionDoesNotSupportLogging = errors.New("session does not support setting logging level") 21 | 22 | // Notification-related errors 23 | ErrNotificationNotInitialized = errors.New("notification channel not initialized") 24 | ErrNotificationChannelBlocked = errors.New("notification channel full or blocked") 25 | ) 26 | 27 | // ErrDynamicPathConfig is returned when attempting to use static path methods with dynamic path configuration 28 | type ErrDynamicPathConfig struct { 29 | Method string 30 | } 31 | 32 | func (e *ErrDynamicPathConfig) Error() string { 33 | return fmt.Sprintf("%s cannot be used with WithDynamicBasePath. Use dynamic path logic in your router.", e.Method) 34 | } 35 | -------------------------------------------------------------------------------- /pkg/mcp/server/http_transport_options.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | // HTTPContextFunc is a function that takes an existing context and the current 9 | // request and returns a potentially modified context based on the request 10 | // content. This can be used to inject context values from headers, for example. 11 | type HTTPContextFunc func(ctx context.Context, r *http.Request) context.Context 12 | -------------------------------------------------------------------------------- /pkg/mcp/server/internal/gen/README.md: -------------------------------------------------------------------------------- 1 | # Readme for Codegen 2 | 3 | This internal module contains code generation for producing a few repetitive 4 | constructs, namely: 5 | 6 | - The switch statement that handles the request dispatch 7 | - The hook function types and the methods on the Hook struct 8 | 9 | To invoke the code generation: 10 | 11 | ``` 12 | go generate ./... 13 | ``` 14 | 15 | ## Development 16 | 17 | - `request_handler.go.tmpl` generates `server/request_handler.go`, and 18 | - `hooks.go.tmpl` generates `server/hooks.go` 19 | 20 | Inside of `data.go` there is a struct with the inputs to both templates. 21 | 22 | Note that the driver in `main.go` generates code and also pipes it through 23 | `goimports` for formatting and imports cleanup. 24 | 25 | -------------------------------------------------------------------------------- /pkg/mcp/server/internal/gen/data.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type MCPRequestType struct { 4 | MethodName string 5 | ParamType string 6 | ResultType string 7 | HookName string 8 | Group string 9 | GroupName string 10 | GroupHookName string 11 | UnmarshalError string 12 | HandlerFunc string 13 | } 14 | 15 | var MCPRequestTypes = []MCPRequestType{ 16 | { 17 | MethodName: "MethodInitialize", 18 | ParamType: "InitializeRequest", 19 | ResultType: "InitializeResult", 20 | HookName: "Initialize", 21 | UnmarshalError: "invalid initialize request", 22 | HandlerFunc: "handleInitialize", 23 | }, { 24 | MethodName: "MethodPing", 25 | ParamType: "PingRequest", 26 | ResultType: "EmptyResult", 27 | HookName: "Ping", 28 | UnmarshalError: "invalid ping request", 29 | HandlerFunc: "handlePing", 30 | }, { 31 | MethodName: "MethodSetLogLevel", 32 | ParamType: "SetLevelRequest", 33 | ResultType: "EmptyResult", 34 | Group: "logging", 35 | GroupName: "Logging", 36 | GroupHookName: "Logging", 37 | HookName: "SetLevel", 38 | UnmarshalError: "invalid set level request", 39 | HandlerFunc: "handleSetLevel", 40 | }, { 41 | MethodName: "MethodResourcesList", 42 | ParamType: "ListResourcesRequest", 43 | ResultType: "ListResourcesResult", 44 | Group: "resources", 45 | GroupName: "Resources", 46 | GroupHookName: "Resource", 47 | HookName: "ListResources", 48 | UnmarshalError: "invalid list resources request", 49 | HandlerFunc: "handleListResources", 50 | }, { 51 | MethodName: "MethodResourcesTemplatesList", 52 | ParamType: "ListResourceTemplatesRequest", 53 | ResultType: "ListResourceTemplatesResult", 54 | Group: "resources", 55 | GroupName: "Resources", 56 | GroupHookName: "Resource", 57 | HookName: "ListResourceTemplates", 58 | UnmarshalError: "invalid list resource templates request", 59 | HandlerFunc: "handleListResourceTemplates", 60 | }, { 61 | MethodName: "MethodResourcesRead", 62 | ParamType: "ReadResourceRequest", 63 | ResultType: "ReadResourceResult", 64 | Group: "resources", 65 | GroupName: "Resources", 66 | GroupHookName: "Resource", 67 | HookName: "ReadResource", 68 | UnmarshalError: "invalid read resource request", 69 | HandlerFunc: "handleReadResource", 70 | }, { 71 | MethodName: "MethodPromptsList", 72 | ParamType: "ListPromptsRequest", 73 | ResultType: "ListPromptsResult", 74 | Group: "prompts", 75 | GroupName: "Prompts", 76 | GroupHookName: "Prompt", 77 | HookName: "ListPrompts", 78 | UnmarshalError: "invalid list prompts request", 79 | HandlerFunc: "handleListPrompts", 80 | }, { 81 | MethodName: "MethodPromptsGet", 82 | ParamType: "GetPromptRequest", 83 | ResultType: "GetPromptResult", 84 | Group: "prompts", 85 | GroupName: "Prompts", 86 | GroupHookName: "Prompt", 87 | HookName: "GetPrompt", 88 | UnmarshalError: "invalid get prompt request", 89 | HandlerFunc: "handleGetPrompt", 90 | }, { 91 | MethodName: "MethodToolsList", 92 | ParamType: "ListToolsRequest", 93 | ResultType: "ListToolsResult", 94 | Group: "tools", 95 | GroupName: "Tools", 96 | GroupHookName: "Tool", 97 | HookName: "ListTools", 98 | UnmarshalError: "invalid list tools request", 99 | HandlerFunc: "handleListTools", 100 | }, { 101 | MethodName: "MethodToolsCall", 102 | ParamType: "CallToolRequest", 103 | ResultType: "CallToolResult", 104 | Group: "tools", 105 | GroupName: "Tools", 106 | GroupHookName: "Tool", 107 | HookName: "CallTool", 108 | UnmarshalError: "invalid call tool request", 109 | HandlerFunc: "handleToolCall", 110 | }, 111 | } 112 | -------------------------------------------------------------------------------- /pkg/mcp/server/internal/gen/hooks.go.tmpl: -------------------------------------------------------------------------------- 1 | // Code generated by `go generate`. DO NOT EDIT. 2 | // source: server/internal/gen/hooks.go.tmpl 3 | package server 4 | 5 | import ( 6 | "context" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | 11 | "github.com/jedisct1/openapi-mcp/internal/mcp-go/mcp" 12 | ) 13 | 14 | // OnRegisterSessionHookFunc is a hook that will be called when a new session is registered. 15 | type OnRegisterSessionHookFunc func(ctx context.Context, session ClientSession) 16 | 17 | // OnUnregisterSessionHookFunc is a hook that will be called when a session is being unregistered. 18 | type OnUnregisterSessionHookFunc func(ctx context.Context, session ClientSession) 19 | 20 | // BeforeAnyHookFunc is a function that is called after the request is 21 | // parsed but before the method is called. 22 | type BeforeAnyHookFunc func(ctx context.Context, id any, method mcp.MCPMethod, message any) 23 | 24 | // OnSuccessHookFunc is a hook that will be called after the request 25 | // successfully generates a result, but before the result is sent to the client. 26 | type OnSuccessHookFunc func(ctx context.Context, id any, method mcp.MCPMethod, message any, result any) 27 | 28 | // OnErrorHookFunc is a hook that will be called when an error occurs, 29 | // either during the request parsing or the method execution. 30 | // 31 | // Example usage: 32 | // ``` 33 | // hooks.AddOnError(func(ctx context.Context, id any, method mcp.MCPMethod, message any, err error) { 34 | // // Check for specific error types using errors.Is 35 | // if errors.Is(err, ErrUnsupported) { 36 | // // Handle capability not supported errors 37 | // log.Printf("Capability not supported: %v", err) 38 | // } 39 | // 40 | // // Use errors.As to get specific error types 41 | // var parseErr = &UnparsableMessageError{} 42 | // if errors.As(err, &parseErr) { 43 | // // Access specific methods/fields of the error type 44 | // log.Printf("Failed to parse message for method %s: %v", 45 | // parseErr.GetMethod(), parseErr.Unwrap()) 46 | // // Access the raw message that failed to parse 47 | // rawMsg := parseErr.GetMessage() 48 | // } 49 | // 50 | // // Check for specific resource/prompt/tool errors 51 | // switch { 52 | // case errors.Is(err, ErrResourceNotFound): 53 | // log.Printf("Resource not found: %v", err) 54 | // case errors.Is(err, ErrPromptNotFound): 55 | // log.Printf("Prompt not found: %v", err) 56 | // case errors.Is(err, ErrToolNotFound): 57 | // log.Printf("Tool not found: %v", err) 58 | // } 59 | // }) 60 | type OnErrorHookFunc func(ctx context.Context, id any, method mcp.MCPMethod, message any, err error) 61 | 62 | // OnRequestInitializationFunc is a function that called before handle diff request method 63 | // Should any errors arise during func execution, the service will promptly return the corresponding error message. 64 | type OnRequestInitializationFunc func(ctx context.Context, id any, message any) error 65 | 66 | 67 | {{range .}} 68 | type OnBefore{{.HookName}}Func func(ctx context.Context, id any, message *mcp.{{.ParamType}}) 69 | type OnAfter{{.HookName}}Func func(ctx context.Context, id any, message *mcp.{{.ParamType}}, result *mcp.{{.ResultType}}) 70 | {{end}} 71 | 72 | type Hooks struct { 73 | OnRegisterSession []OnRegisterSessionHookFunc 74 | OnUnregisterSession []OnUnregisterSessionHookFunc 75 | OnBeforeAny []BeforeAnyHookFunc 76 | OnSuccess []OnSuccessHookFunc 77 | OnError []OnErrorHookFunc 78 | OnRequestInitialization []OnRequestInitializationFunc 79 | {{- range .}} 80 | OnBefore{{.HookName}} []OnBefore{{.HookName}}Func 81 | OnAfter{{.HookName}} []OnAfter{{.HookName}}Func 82 | {{- end}} 83 | } 84 | 85 | func (c *Hooks) AddBeforeAny(hook BeforeAnyHookFunc) { 86 | c.OnBeforeAny = append(c.OnBeforeAny, hook) 87 | } 88 | 89 | func (c *Hooks) AddOnSuccess(hook OnSuccessHookFunc) { 90 | c.OnSuccess = append(c.OnSuccess, hook) 91 | } 92 | 93 | // AddOnError registers a hook function that will be called when an error occurs. 94 | // The error parameter contains the actual error object, which can be interrogated 95 | // using Go's error handling patterns like errors.Is and errors.As. 96 | // 97 | // Example: 98 | // ``` 99 | // // Create a channel to receive errors for testing 100 | // errChan := make(chan error, 1) 101 | // 102 | // // Register hook to capture and inspect errors 103 | // hooks := &Hooks{} 104 | // hooks.AddOnError(func(ctx context.Context, id any, method mcp.MCPMethod, message any, err error) { 105 | // // For capability-related errors 106 | // if errors.Is(err, ErrUnsupported) { 107 | // // Handle capability not supported 108 | // errChan <- err 109 | // return 110 | // } 111 | // 112 | // // For parsing errors 113 | // var parseErr = &UnparsableMessageError{} 114 | // if errors.As(err, &parseErr) { 115 | // // Handle unparsable message errors 116 | // fmt.Printf("Failed to parse %s request: %v\n", 117 | // parseErr.GetMethod(), parseErr.Unwrap()) 118 | // errChan <- parseErr 119 | // return 120 | // } 121 | // 122 | // // For resource/prompt/tool not found errors 123 | // if errors.Is(err, ErrResourceNotFound) || 124 | // errors.Is(err, ErrPromptNotFound) || 125 | // errors.Is(err, ErrToolNotFound) { 126 | // // Handle not found errors 127 | // errChan <- err 128 | // return 129 | // } 130 | // 131 | // // For other errors 132 | // errChan <- err 133 | // }) 134 | // 135 | // server := NewMCPServer("test-server", "1.0.0", WithHooks(hooks)) 136 | // ``` 137 | func (c *Hooks) AddOnError(hook OnErrorHookFunc) { 138 | c.OnError = append(c.OnError, hook) 139 | } 140 | 141 | func (c *Hooks) beforeAny(ctx context.Context, id any, method mcp.MCPMethod, message any) { 142 | if c == nil { 143 | return 144 | } 145 | for _, hook := range c.OnBeforeAny { 146 | hook(ctx, id, method, message) 147 | } 148 | } 149 | 150 | func (c *Hooks) onSuccess(ctx context.Context, id any, method mcp.MCPMethod, message any, result any) { 151 | if c == nil { 152 | return 153 | } 154 | for _, hook := range c.OnSuccess { 155 | hook(ctx, id, method, message, result) 156 | } 157 | } 158 | 159 | // onError calls all registered error hooks with the error object. 160 | // The err parameter contains the actual error that occurred, which implements 161 | // the standard error interface and may be a wrapped error or custom error type. 162 | // 163 | // This allows consumer code to use Go's error handling patterns: 164 | // - errors.Is(err, ErrUnsupported) to check for specific sentinel errors 165 | // - errors.As(err, &customErr) to extract custom error types 166 | // 167 | // Common error types include: 168 | // - ErrUnsupported: When a capability is not enabled 169 | // - UnparsableMessageError: When request parsing fails 170 | // - ErrResourceNotFound: When a resource is not found 171 | // - ErrPromptNotFound: When a prompt is not found 172 | // - ErrToolNotFound: When a tool is not found 173 | func (c *Hooks) onError(ctx context.Context, id any, method mcp.MCPMethod, message any, err error) { 174 | if c == nil { 175 | return 176 | } 177 | for _, hook := range c.OnError { 178 | hook(ctx, id, method, message, err) 179 | } 180 | } 181 | 182 | func (c *Hooks) AddOnRegisterSession(hook OnRegisterSessionHookFunc) { 183 | c.OnRegisterSession = append(c.OnRegisterSession, hook) 184 | } 185 | 186 | func (c *Hooks) RegisterSession(ctx context.Context, session ClientSession) { 187 | if c == nil { 188 | return 189 | } 190 | for _, hook := range c.OnRegisterSession { 191 | hook(ctx, session) 192 | } 193 | } 194 | 195 | func (c *Hooks) AddOnUnregisterSession(hook OnUnregisterSessionHookFunc) { 196 | c.OnUnregisterSession = append(c.OnUnregisterSession, hook) 197 | } 198 | 199 | func (c *Hooks) UnregisterSession(ctx context.Context, session ClientSession) { 200 | if c == nil { 201 | return 202 | } 203 | for _, hook := range c.OnUnregisterSession { 204 | hook(ctx, session) 205 | } 206 | } 207 | 208 | func (c *Hooks) AddOnRequestInitialization(hook OnRequestInitializationFunc) { 209 | c.OnRequestInitialization = append(c.OnRequestInitialization, hook) 210 | } 211 | 212 | func (c *Hooks) onRequestInitialization(ctx context.Context, id any, message any) error { 213 | if c == nil { 214 | return nil 215 | } 216 | for _, hook := range c.OnRequestInitialization { 217 | err := hook(ctx, id, message) 218 | if err != nil { 219 | return err 220 | } 221 | } 222 | return nil 223 | } 224 | 225 | {{- range .}} 226 | func (c *Hooks) AddBefore{{.HookName}}(hook OnBefore{{.HookName}}Func) { 227 | c.OnBefore{{.HookName}} = append(c.OnBefore{{.HookName}}, hook) 228 | } 229 | 230 | func (c *Hooks) AddAfter{{.HookName}}(hook OnAfter{{.HookName}}Func) { 231 | c.OnAfter{{.HookName}} = append(c.OnAfter{{.HookName}}, hook) 232 | } 233 | 234 | func (c *Hooks) before{{.HookName}}(ctx context.Context, id any, message *mcp.{{.ParamType}}) { 235 | c.beforeAny(ctx, id, mcp.{{.MethodName}}, message) 236 | if c == nil { 237 | return 238 | } 239 | for _, hook := range c.OnBefore{{.HookName}} { 240 | hook(ctx, id, message) 241 | } 242 | } 243 | 244 | func (c *Hooks) after{{.HookName}}(ctx context.Context, id any, message *mcp.{{.ParamType}}, result *mcp.{{.ResultType}}) { 245 | c.onSuccess(ctx, id, mcp.{{.MethodName}}, message, result) 246 | if c == nil { 247 | return 248 | } 249 | for _, hook := range c.OnAfter{{.HookName}} { 250 | hook(ctx, id, message, result) 251 | } 252 | } 253 | {{- end -}} 254 | -------------------------------------------------------------------------------- /pkg/mcp/server/internal/gen/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "strings" 12 | "text/template" 13 | ) 14 | 15 | //go:generate go run . ../.. 16 | 17 | //go:embed hooks.go.tmpl 18 | var hooksTemplate string 19 | 20 | //go:embed request_handler.go.tmpl 21 | var requestHandlerTemplate string 22 | 23 | func RenderTemplateToFile(templateContent, destPath, fileName string, data any) error { 24 | // Create temp file for initial output 25 | tempFile, err := os.CreateTemp("", "hooks-*.go") 26 | if err != nil { 27 | return err 28 | } 29 | tempFilePath := tempFile.Name() 30 | defer os.Remove(tempFilePath) // Clean up temp file when done 31 | defer tempFile.Close() 32 | 33 | // Parse and execute template to temp file 34 | tmpl, err := template.New(fileName).Funcs(template.FuncMap{ 35 | "toLower": strings.ToLower, 36 | }).Parse(templateContent) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | if err := tmpl.Execute(tempFile, data); err != nil { 42 | return err 43 | } 44 | 45 | // Run goimports on the temp file 46 | cmd := exec.Command("go", "run", "golang.org/x/tools/cmd/goimports@latest", "-w", tempFilePath) 47 | if output, err := cmd.CombinedOutput(); err != nil { 48 | return fmt.Errorf("goimports failed: %v\n%s", err, output) 49 | } 50 | 51 | // Read the processed content 52 | processedContent, err := os.ReadFile(tempFilePath) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | // Write the processed content to the destination 58 | var destWriter io.Writer 59 | if destPath == "-" { 60 | destWriter = os.Stdout 61 | } else { 62 | destFile, err := os.Create(filepath.Join(destPath, fileName)) 63 | if err != nil { 64 | return err 65 | } 66 | defer destFile.Close() 67 | destWriter = destFile 68 | } 69 | 70 | _, err = destWriter.Write(processedContent) 71 | return err 72 | } 73 | 74 | func main() { 75 | if len(os.Args) < 2 { 76 | log.Fatal("usage: gen ") 77 | } 78 | destPath := os.Args[1] 79 | 80 | if err := RenderTemplateToFile(hooksTemplate, destPath, "hooks.go", MCPRequestTypes); err != nil { 81 | log.Fatal(err) 82 | } 83 | 84 | if err := RenderTemplateToFile(requestHandlerTemplate, destPath, "request_handler.go", MCPRequestTypes); err != nil { 85 | log.Fatal(err) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /pkg/mcp/server/internal/gen/request_handler.go.tmpl: -------------------------------------------------------------------------------- 1 | // Code generated by `go generate`. DO NOT EDIT. 2 | // source: server/internal/gen/request_handler.go.tmpl 3 | package server 4 | 5 | import ( 6 | "context" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | 11 | "github.com/jedisct1/openapi-mcp/internal/mcp-go/mcp" 12 | ) 13 | 14 | // HandleMessage processes an incoming JSON-RPC message and returns an appropriate response 15 | func (s *MCPServer) HandleMessage( 16 | ctx context.Context, 17 | message json.RawMessage, 18 | ) mcp.JSONRPCMessage { 19 | // Add server to context 20 | ctx = context.WithValue(ctx, serverKey{}, s) 21 | var err *requestError 22 | 23 | var baseMessage struct { 24 | JSONRPC string `json:"jsonrpc"` 25 | Method mcp.MCPMethod `json:"method"` 26 | ID any `json:"id,omitempty"` 27 | Result any `json:"result,omitempty"` 28 | } 29 | 30 | if err := json.Unmarshal(message, &baseMessage); err != nil { 31 | return createErrorResponse( 32 | nil, 33 | mcp.PARSE_ERROR, 34 | "Failed to parse message", 35 | ) 36 | } 37 | 38 | // Check for valid JSONRPC version 39 | if baseMessage.JSONRPC != mcp.JSONRPC_VERSION { 40 | return createErrorResponse( 41 | baseMessage.ID, 42 | mcp.INVALID_REQUEST, 43 | "Invalid JSON-RPC version", 44 | ) 45 | } 46 | 47 | if baseMessage.ID == nil { 48 | var notification mcp.JSONRPCNotification 49 | if err := json.Unmarshal(message, ¬ification); err != nil { 50 | return createErrorResponse( 51 | nil, 52 | mcp.PARSE_ERROR, 53 | "Failed to parse notification", 54 | ) 55 | } 56 | s.handleNotification(ctx, notification) 57 | return nil // Return nil for notifications 58 | } 59 | 60 | if baseMessage.Result != nil { 61 | // this is a response to a request sent by the server (e.g. from a ping 62 | // sent due to WithKeepAlive option) 63 | return nil 64 | } 65 | 66 | handleErr := s.hooks.onRequestInitialization(ctx, baseMessage.ID, message) 67 | if handleErr != nil { 68 | return createErrorResponse( 69 | baseMessage.ID, 70 | mcp.INVALID_REQUEST, 71 | handleErr.Error(), 72 | ) 73 | } 74 | 75 | switch baseMessage.Method { 76 | {{- range .}} 77 | case mcp.{{.MethodName}}: 78 | var request mcp.{{.ParamType}} 79 | var result *mcp.{{.ResultType}} 80 | {{ if .Group }}if s.capabilities.{{.Group}} == nil { 81 | err = &requestError{ 82 | id: baseMessage.ID, 83 | code: mcp.METHOD_NOT_FOUND, 84 | err: fmt.Errorf("{{toLower .GroupName}} %w", ErrUnsupported), 85 | } 86 | } else{{ end }} if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil { 87 | err = &requestError{ 88 | id: baseMessage.ID, 89 | code: mcp.INVALID_REQUEST, 90 | err: &UnparsableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method}, 91 | } 92 | } else { 93 | s.hooks.before{{.HookName}}(ctx, baseMessage.ID, &request) 94 | result, err = s.{{.HandlerFunc}}(ctx, baseMessage.ID, request) 95 | } 96 | if err != nil { 97 | s.hooks.onError(ctx, baseMessage.ID, baseMessage.Method, &request, err) 98 | return err.ToJSONRPCError() 99 | } 100 | s.hooks.after{{.HookName}}(ctx, baseMessage.ID, &request, result) 101 | return createResponse(baseMessage.ID, *result) 102 | {{- end }} 103 | default: 104 | return createErrorResponse( 105 | baseMessage.ID, 106 | mcp.METHOD_NOT_FOUND, 107 | fmt.Sprintf("Method %s not found", baseMessage.Method), 108 | ) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /pkg/mcp/server/server_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "testing" 7 | 8 | "github.com/jedisct1/openapi-mcp/pkg/mcp/mcp" 9 | ) 10 | 11 | func TestMCPServer_ProtocolNegotiation(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | clientVersion string 15 | expectedVersion string 16 | }{ 17 | { 18 | name: "Server supports client version - should respond with same version", 19 | clientVersion: "2024-11-05", 20 | expectedVersion: "2024-11-05", 21 | }, 22 | { 23 | name: "Client requests current latest - should respond with same version", 24 | clientVersion: mcp.LATEST_PROTOCOL_VERSION, 25 | expectedVersion: mcp.LATEST_PROTOCOL_VERSION, 26 | }, 27 | { 28 | name: "Client requests unsupported future version - should respond with server's latest", 29 | clientVersion: "2026-01-01", 30 | expectedVersion: mcp.LATEST_PROTOCOL_VERSION, 31 | }, 32 | { 33 | name: "Client requests unsupported old version - should respond with server's latest", 34 | clientVersion: "2023-01-01", 35 | expectedVersion: mcp.LATEST_PROTOCOL_VERSION, 36 | }, 37 | } 38 | 39 | for _, tt := range tests { 40 | t.Run(tt.name, func(t *testing.T) { 41 | server := NewMCPServer("test-server", "1.0.0") 42 | 43 | params := struct { 44 | ProtocolVersion string `json:"protocolVersion"` 45 | ClientInfo mcp.Implementation `json:"clientInfo"` 46 | Capabilities mcp.ClientCapabilities `json:"capabilities"` 47 | }{ 48 | ProtocolVersion: tt.clientVersion, 49 | ClientInfo: mcp.Implementation{ 50 | Name: "test-client", 51 | Version: "1.0.0", 52 | }, 53 | } 54 | 55 | // Create initialize request with specific protocol version 56 | initRequest := mcp.JSONRPCRequest{ 57 | JSONRPC: "2.0", 58 | ID: mcp.NewRequestId(int64(1)), 59 | Request: mcp.Request{ 60 | Method: "initialize", 61 | }, 62 | Params: params, 63 | } 64 | 65 | messageBytes, err := json.Marshal(initRequest) 66 | if err != nil { 67 | t.Fatalf("Failed to marshal request: %v", err) 68 | } 69 | 70 | response := server.HandleMessage(context.Background(), messageBytes) 71 | if response == nil { 72 | t.Fatalf("No response from server") 73 | } 74 | 75 | resp, ok := response.(mcp.JSONRPCResponse) 76 | if !ok { 77 | t.Fatalf("Response is not JSONRPCResponse: %T", response) 78 | } 79 | 80 | initResult, ok := resp.Result.(mcp.InitializeResult) 81 | if !ok { 82 | t.Fatalf("Result is not InitializeResult: %T", resp.Result) 83 | } 84 | 85 | if initResult.ProtocolVersion != tt.expectedVersion { 86 | t.Errorf("ProtocolVersion = %q, want %q", initResult.ProtocolVersion, tt.expectedVersion) 87 | } 88 | }) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /pkg/mcp/server/stdio.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "log" 10 | "os" 11 | "os/signal" 12 | "sync/atomic" 13 | "syscall" 14 | 15 | "github.com/jedisct1/openapi-mcp/pkg/mcp/mcp" 16 | ) 17 | 18 | // StdioContextFunc is a function that takes an existing context and returns 19 | // a potentially modified context. 20 | // This can be used to inject context values from environment variables, 21 | // for example. 22 | type StdioContextFunc func(ctx context.Context) context.Context 23 | 24 | // StdioServer wraps a MCPServer and handles stdio communication. 25 | // It provides a simple way to create command-line MCP servers that 26 | // communicate via standard input/output streams using JSON-RPC messages. 27 | type StdioServer struct { 28 | server *MCPServer 29 | errLogger *log.Logger 30 | contextFunc StdioContextFunc 31 | } 32 | 33 | // StdioOption defines a function type for configuring StdioServer 34 | type StdioOption func(*StdioServer) 35 | 36 | // WithErrorLogger sets the error logger for the server 37 | func WithErrorLogger(logger *log.Logger) StdioOption { 38 | return func(s *StdioServer) { 39 | s.errLogger = logger 40 | } 41 | } 42 | 43 | // WithStdioContextFunc sets a function that will be called to customise the context 44 | // to the server. Note that the stdio server uses the same context for all requests, 45 | // so this function will only be called once per server instance. 46 | func WithStdioContextFunc(fn StdioContextFunc) StdioOption { 47 | return func(s *StdioServer) { 48 | s.contextFunc = fn 49 | } 50 | } 51 | 52 | // stdioSession is a static client session, since stdio has only one client. 53 | type stdioSession struct { 54 | notifications chan mcp.JSONRPCNotification 55 | initialized atomic.Bool 56 | loggingLevel atomic.Value 57 | } 58 | 59 | func (s *stdioSession) SessionID() string { 60 | return "stdio" 61 | } 62 | 63 | func (s *stdioSession) NotificationChannel() chan<- mcp.JSONRPCNotification { 64 | return s.notifications 65 | } 66 | 67 | func (s *stdioSession) Initialize() { 68 | // set default logging level 69 | s.loggingLevel.Store(mcp.LoggingLevelError) 70 | s.initialized.Store(true) 71 | } 72 | 73 | func (s *stdioSession) Initialized() bool { 74 | return s.initialized.Load() 75 | } 76 | 77 | func (s *stdioSession) SetLogLevel(level mcp.LoggingLevel) { 78 | s.loggingLevel.Store(level) 79 | } 80 | 81 | func (s *stdioSession) GetLogLevel() mcp.LoggingLevel { 82 | level := s.loggingLevel.Load() 83 | if level == nil { 84 | return mcp.LoggingLevelError 85 | } 86 | return level.(mcp.LoggingLevel) 87 | } 88 | 89 | var ( 90 | _ ClientSession = (*stdioSession)(nil) 91 | _ SessionWithLogging = (*stdioSession)(nil) 92 | ) 93 | 94 | var stdioSessionInstance = stdioSession{ 95 | notifications: make(chan mcp.JSONRPCNotification, 100), 96 | } 97 | 98 | // NewStdioServer creates a new stdio server wrapper around an MCPServer. 99 | // It initializes the server with a default error logger that discards all output. 100 | func NewStdioServer(server *MCPServer) *StdioServer { 101 | return &StdioServer{ 102 | server: server, 103 | errLogger: log.New( 104 | os.Stderr, 105 | "", 106 | log.LstdFlags, 107 | ), // Default to discarding logs 108 | } 109 | } 110 | 111 | // SetErrorLogger configures where error messages from the StdioServer are logged. 112 | // The provided logger will receive all error messages generated during server operation. 113 | func (s *StdioServer) SetErrorLogger(logger *log.Logger) { 114 | s.errLogger = logger 115 | } 116 | 117 | // SetContextFunc sets a function that will be called to customise the context 118 | // to the server. Note that the stdio server uses the same context for all requests, 119 | // so this function will only be called once per server instance. 120 | func (s *StdioServer) SetContextFunc(fn StdioContextFunc) { 121 | s.contextFunc = fn 122 | } 123 | 124 | // handleNotifications continuously processes notifications from the session's notification channel 125 | // and writes them to the provided output. It runs until the context is cancelled. 126 | // Any errors encountered while writing notifications are logged but do not stop the handler. 127 | func (s *StdioServer) handleNotifications(ctx context.Context, stdout io.Writer) { 128 | for { 129 | select { 130 | case notification := <-stdioSessionInstance.notifications: 131 | if err := s.writeResponse(notification, stdout); err != nil { 132 | s.errLogger.Printf("Error writing notification: %v", err) 133 | } 134 | case <-ctx.Done(): 135 | return 136 | } 137 | } 138 | } 139 | 140 | // processInputStream continuously reads and processes messages from the input stream. 141 | // It handles EOF gracefully as a normal termination condition. 142 | // The function returns when either: 143 | // - The context is cancelled (returns context.Err()) 144 | // - EOF is encountered (returns nil) 145 | // - An error occurs while reading or processing messages (returns the error) 146 | func (s *StdioServer) processInputStream(ctx context.Context, reader *bufio.Reader, stdout io.Writer) error { 147 | for { 148 | if err := ctx.Err(); err != nil { 149 | return err 150 | } 151 | 152 | line, err := s.readNextLine(ctx, reader) 153 | if err != nil { 154 | if err == io.EOF { 155 | return nil 156 | } 157 | s.errLogger.Printf("Error reading input: %v", err) 158 | return err 159 | } 160 | 161 | if err := s.processMessage(ctx, line, stdout); err != nil { 162 | if err == io.EOF { 163 | return nil 164 | } 165 | s.errLogger.Printf("Error handling message: %v", err) 166 | return err 167 | } 168 | } 169 | } 170 | 171 | // readNextLine reads a single line from the input reader in a context-aware manner. 172 | // It uses channels to make the read operation cancellable via context. 173 | // Returns the read line and any error encountered. If the context is cancelled, 174 | // returns an empty string and the context's error. EOF is returned when the input 175 | // stream is closed. 176 | func (s *StdioServer) readNextLine(ctx context.Context, reader *bufio.Reader) (string, error) { 177 | readChan := make(chan string, 1) 178 | errChan := make(chan error, 1) 179 | done := make(chan struct{}) 180 | defer close(done) 181 | 182 | go func() { 183 | select { 184 | case <-done: 185 | return 186 | default: 187 | line, err := reader.ReadString('\n') 188 | if err != nil { 189 | select { 190 | case errChan <- err: 191 | case <-done: 192 | } 193 | return 194 | } 195 | select { 196 | case readChan <- line: 197 | case <-done: 198 | } 199 | return 200 | } 201 | }() 202 | 203 | select { 204 | case <-ctx.Done(): 205 | return "", ctx.Err() 206 | case err := <-errChan: 207 | return "", err 208 | case line := <-readChan: 209 | return line, nil 210 | } 211 | } 212 | 213 | // Listen starts listening for JSON-RPC messages on the provided input and writes responses to the provided output. 214 | // It runs until the context is cancelled or an error occurs. 215 | // Returns an error if there are issues with reading input or writing output. 216 | func (s *StdioServer) Listen( 217 | ctx context.Context, 218 | stdin io.Reader, 219 | stdout io.Writer, 220 | ) error { 221 | // Set a static client context since stdio only has one client 222 | if err := s.server.RegisterSession(ctx, &stdioSessionInstance); err != nil { 223 | return fmt.Errorf("register session: %w", err) 224 | } 225 | defer s.server.UnregisterSession(ctx, stdioSessionInstance.SessionID()) 226 | ctx = s.server.WithContext(ctx, &stdioSessionInstance) 227 | 228 | // Add in any custom context. 229 | if s.contextFunc != nil { 230 | ctx = s.contextFunc(ctx) 231 | } 232 | 233 | reader := bufio.NewReader(stdin) 234 | 235 | // Start notification handler 236 | go s.handleNotifications(ctx, stdout) 237 | return s.processInputStream(ctx, reader, stdout) 238 | } 239 | 240 | // processMessage handles a single JSON-RPC message and writes the response. 241 | // It parses the message, processes it through the wrapped MCPServer, and writes any response. 242 | // Returns an error if there are issues with message processing or response writing. 243 | func (s *StdioServer) processMessage( 244 | ctx context.Context, 245 | line string, 246 | writer io.Writer, 247 | ) error { 248 | // Parse the message as raw JSON 249 | var rawMessage json.RawMessage 250 | if err := json.Unmarshal([]byte(line), &rawMessage); err != nil { 251 | response := createErrorResponse(nil, mcp.PARSE_ERROR, "Parse error") 252 | return s.writeResponse(response, writer) 253 | } 254 | 255 | // Handle the message using the wrapped server 256 | response := s.server.HandleMessage(ctx, rawMessage) 257 | 258 | // Only write response if there is one (not for notifications) 259 | if response != nil { 260 | if err := s.writeResponse(response, writer); err != nil { 261 | return fmt.Errorf("failed to write response: %w", err) 262 | } 263 | } 264 | 265 | return nil 266 | } 267 | 268 | // writeResponse marshals and writes a JSON-RPC response message followed by a newline. 269 | // Returns an error if marshaling or writing fails. 270 | func (s *StdioServer) writeResponse( 271 | response mcp.JSONRPCMessage, 272 | writer io.Writer, 273 | ) error { 274 | responseBytes, err := json.Marshal(response) 275 | if err != nil { 276 | return err 277 | } 278 | 279 | // Write response followed by newline 280 | if _, err := fmt.Fprintf(writer, "%s\n", responseBytes); err != nil { 281 | return err 282 | } 283 | 284 | return nil 285 | } 286 | 287 | // ServeStdio is a convenience function that creates and starts a StdioServer with os.Stdin and os.Stdout. 288 | // It sets up signal handling for graceful shutdown on SIGTERM and SIGINT. 289 | // Returns an error if the server encounters any issues during operation. 290 | func ServeStdio(server *MCPServer, opts ...StdioOption) error { 291 | s := NewStdioServer(server) 292 | 293 | for _, opt := range opts { 294 | opt(s) 295 | } 296 | 297 | ctx, cancel := context.WithCancel(context.Background()) 298 | defer cancel() 299 | 300 | // Set up signal handling 301 | sigChan := make(chan os.Signal, 1) 302 | signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT) 303 | 304 | go func() { 305 | <-sigChan 306 | cancel() 307 | }() 308 | 309 | return s.Listen(ctx, os.Stdin, os.Stdout) 310 | } 311 | -------------------------------------------------------------------------------- /pkg/mcp/server/streamable_http_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "encoding/json" 7 | "net/http" 8 | "net/http/httptest" 9 | "strings" 10 | "testing" 11 | "time" 12 | 13 | "github.com/jedisct1/openapi-mcp/pkg/mcp/mcp" 14 | ) 15 | 16 | func TestStreamableHTTPServer_GET(t *testing.T) { 17 | // Create a basic MCP server 18 | mcpServer := NewMCPServer("test-server", "1.0.0") 19 | 20 | // Create the streamable HTTP server 21 | httpServer := NewStreamableHTTPServer(mcpServer) 22 | 23 | // Create a test server 24 | testServer := httptest.NewServer(httpServer) 25 | defer testServer.Close() 26 | 27 | t.Run("GET request establishes SSE connection", func(t *testing.T) { 28 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 29 | defer cancel() 30 | 31 | req, err := http.NewRequestWithContext(ctx, "GET", testServer.URL, nil) 32 | if err != nil { 33 | t.Fatalf("Failed to create request: %v", err) 34 | } 35 | 36 | resp, err := http.DefaultClient.Do(req) 37 | if err != nil { 38 | t.Fatalf("Failed to send GET request: %v", err) 39 | } 40 | defer resp.Body.Close() 41 | 42 | // Check status code 43 | if resp.StatusCode != http.StatusAccepted { 44 | t.Errorf("Expected status %d, got %d", http.StatusAccepted, resp.StatusCode) 45 | } 46 | 47 | // Check content type 48 | if resp.Header.Get("Content-Type") != "text/event-stream" { 49 | t.Errorf("Expected Content-Type text/event-stream, got %s", resp.Header.Get("Content-Type")) 50 | } 51 | 52 | // Check cache control 53 | if resp.Header.Get("Cache-Control") != "no-cache" { 54 | t.Errorf("Expected Cache-Control no-cache, got %s", resp.Header.Get("Cache-Control")) 55 | } 56 | 57 | // Check connection header 58 | if resp.Header.Get("Connection") != "keep-alive" { 59 | t.Errorf("Expected Connection keep-alive, got %s", resp.Header.Get("Connection")) 60 | } 61 | 62 | // Read the initial endpoint event 63 | reader := bufio.NewReader(resp.Body) 64 | 65 | // Read event line 66 | eventLine, err := reader.ReadString('\n') 67 | if err != nil { 68 | t.Fatalf("Failed to read event line: %v", err) 69 | } 70 | 71 | if !strings.HasPrefix(eventLine, "event: endpoint") { 72 | t.Errorf("Expected initial event to be 'endpoint', got %s", strings.TrimSpace(eventLine)) 73 | } 74 | 75 | // Read data line 76 | dataLine, err := reader.ReadString('\n') 77 | if err != nil { 78 | t.Fatalf("Failed to read data line: %v", err) 79 | } 80 | 81 | if !strings.HasPrefix(dataLine, "data: ?sessionId=") { 82 | t.Errorf("Expected data line to contain sessionId, got %s", strings.TrimSpace(dataLine)) 83 | } 84 | 85 | // Read empty line 86 | emptyLine, err := reader.ReadString('\n') 87 | if err != nil { 88 | t.Fatalf("Failed to read empty line: %v", err) 89 | } 90 | 91 | if strings.TrimSpace(emptyLine) != "" { 92 | t.Errorf("Expected empty line after SSE event, got %s", strings.TrimSpace(emptyLine)) 93 | } 94 | }) 95 | 96 | t.Run("POST request with notifications upgrades to SSE", func(t *testing.T) { 97 | // Add a tool that sends notifications 98 | mcpServer.AddTool(mcp.Tool{ 99 | Name: "test-tool", 100 | }, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { 101 | // Send a notification to the client 102 | server := ServerFromContext(ctx) 103 | if server != nil { 104 | // Add a small delay to ensure the notification handler is ready 105 | time.Sleep(10 * time.Millisecond) 106 | err := server.SendNotificationToClient(ctx, "test/notification", map[string]any{ 107 | "message": "test notification", 108 | }) 109 | if err != nil { 110 | t.Logf("Failed to send notification: %v", err) 111 | } else { 112 | t.Logf("Notification sent successfully") 113 | } 114 | } else { 115 | t.Logf("Server not found in context") 116 | } 117 | return mcp.NewToolResultText("done", nil, nil, nil, "", nil), nil 118 | }) 119 | 120 | // First, initialize the session 121 | initRequest := map[string]any{ 122 | "jsonrpc": "2.0", 123 | "id": 1, 124 | "method": "initialize", 125 | "params": map[string]any{ 126 | "protocolVersion": "2025-03-26", 127 | "clientInfo": map[string]any{ 128 | "name": "test-client", 129 | "version": "1.0.0", 130 | }, 131 | }, 132 | } 133 | 134 | initBody, _ := json.Marshal(initRequest) 135 | resp, err := http.Post(testServer.URL, "application/json", strings.NewReader(string(initBody))) 136 | if err != nil { 137 | t.Fatalf("Failed to send initialize request: %v", err) 138 | } 139 | defer resp.Body.Close() 140 | 141 | if resp.StatusCode != http.StatusOK { 142 | t.Errorf("Expected status 200 for initialize, got %d", resp.StatusCode) 143 | } 144 | 145 | sessionID := resp.Header.Get("Mcp-Session-Id") 146 | if sessionID == "" { 147 | t.Fatal("Expected session ID in response header") 148 | } 149 | 150 | // Now call the tool that sends notifications 151 | toolRequest := map[string]any{ 152 | "jsonrpc": "2.0", 153 | "id": 2, 154 | "method": "tools/call", 155 | "params": map[string]any{ 156 | "name": "test-tool", 157 | }, 158 | } 159 | 160 | toolBody, _ := json.Marshal(toolRequest) 161 | req, err := http.NewRequest("POST", testServer.URL, strings.NewReader(string(toolBody))) 162 | if err != nil { 163 | t.Fatalf("Failed to create tool request: %v", err) 164 | } 165 | req.Header.Set("Content-Type", "application/json") 166 | req.Header.Set("Mcp-Session-Id", sessionID) 167 | 168 | resp2, err := http.DefaultClient.Do(req) 169 | if err != nil { 170 | t.Fatalf("Failed to send tool request: %v", err) 171 | } 172 | defer resp2.Body.Close() 173 | 174 | // Should upgrade to SSE 175 | if resp2.StatusCode != http.StatusAccepted { 176 | t.Errorf("Expected status 202 for SSE upgrade, got %d", resp2.StatusCode) 177 | } 178 | 179 | if resp2.Header.Get("Content-Type") != "text/event-stream" { 180 | t.Errorf("Expected Content-Type text/event-stream for SSE upgrade, got %s", resp2.Header.Get("Content-Type")) 181 | } 182 | }) 183 | } 184 | -------------------------------------------------------------------------------- /pkg/mcp/util/logger.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | // Logger defines a minimal logging interface 8 | type Logger interface { 9 | Infof(format string, v ...any) 10 | Errorf(format string, v ...any) 11 | } 12 | 13 | // --- Standard Library Logger Wrapper --- 14 | 15 | // DefaultStdLogger implements Logger using the standard library's log.Logger. 16 | func DefaultLogger() Logger { 17 | return &stdLogger{ 18 | logger: log.Default(), 19 | } 20 | } 21 | 22 | // stdLogger wraps the standard library's log.Logger. 23 | type stdLogger struct { 24 | logger *log.Logger 25 | } 26 | 27 | func (l *stdLogger) Infof(format string, v ...any) { 28 | l.logger.Printf("INFO: "+format, v...) 29 | } 30 | 31 | func (l *stdLogger) Errorf(format string, v ...any) { 32 | l.logger.Printf("ERROR: "+format, v...) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/openapi2mcp/README.md: -------------------------------------------------------------------------------- 1 | # openapi2mcp Go Library 2 | 3 | This package provides a Go library for converting OpenAPI 3.x specifications into MCP (Model Context Protocol) tool servers. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | go get github.com/jedisct1/openapi-mcp/pkg/openapi2mcp 9 | ``` 10 | 11 | For direct access to MCP types and tools: 12 | ```bash 13 | go get github.com/jedisct1/openapi-mcp/pkg/mcp 14 | ``` 15 | 16 | ## Usage 17 | 18 | ```go 19 | package main 20 | 21 | import ( 22 | "log" 23 | "github.com/jedisct1/openapi-mcp/pkg/openapi2mcp" 24 | ) 25 | 26 | func main() { 27 | // Load OpenAPI spec 28 | doc, err := openapi2mcp.LoadOpenAPISpec("openapi.yaml") 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | 33 | // Create MCP server 34 | srv := openapi2mcp.NewServer("myapi", doc.Info.Version, doc) 35 | 36 | // Serve over HTTP (StreamableHTTP is now the default) 37 | if err := openapi2mcp.ServeStreamableHTTP(srv, ":8080", "/mcp"); err != nil { 38 | log.Fatal(err) 39 | } 40 | 41 | // Or serve over stdio 42 | // if err := openapi2mcp.ServeStdio(srv); err != nil { 43 | // log.Fatal(err) 44 | // } 45 | } 46 | ``` 47 | 48 | ### Using MCP Package Directly 49 | 50 | For more advanced usage, you can work with MCP types and tools directly: 51 | 52 | ```go 53 | package main 54 | 55 | import ( 56 | "context" 57 | "log" 58 | 59 | "github.com/jedisct1/openapi-mcp/pkg/mcp/mcp" 60 | "github.com/jedisct1/openapi-mcp/pkg/mcp/server" 61 | "github.com/jedisct1/openapi-mcp/pkg/openapi2mcp" 62 | ) 63 | 64 | func main() { 65 | // Load OpenAPI spec 66 | doc, err := openapi2mcp.LoadOpenAPISpec("openapi.yaml") 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | 71 | // Create MCP server manually 72 | srv := server.NewMCPServer("myapi", doc.Info.Version) 73 | 74 | // Register OpenAPI tools 75 | ops := openapi2mcp.ExtractOpenAPIOperations(doc) 76 | openapi2mcp.RegisterOpenAPITools(srv, ops, doc, nil) 77 | 78 | // Add custom tools using the MCP package directly 79 | customTool := mcp.NewTool("custom", 80 | mcp.WithDescription("A custom tool"), 81 | mcp.WithString("message", mcp.Description("Message to process"), mcp.Required()), 82 | ) 83 | 84 | srv.AddTool(customTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { 85 | args := req.GetArguments() 86 | message := args["message"].(string) 87 | 88 | return &mcp.CallToolResult{ 89 | Content: []mcp.Content{ 90 | mcp.TextContent{ 91 | Type: "text", 92 | Text: "Processed: " + message, 93 | }, 94 | }, 95 | }, nil 96 | }) 97 | 98 | // Serve 99 | if err := server.ServeStdio(srv); err != nil { 100 | log.Fatal(err) 101 | } 102 | } 103 | ``` 104 | 105 | ## Features 106 | 107 | - Convert OpenAPI 3.x specifications to MCP tool servers 108 | - Support for HTTP (StreamableHTTP is default, SSE also available) and stdio transport 109 | - Automatic tool generation from OpenAPI operations 110 | - Built-in validation and error handling 111 | - AI-optimized responses with structured output 112 | 113 | ## API Documentation 114 | 115 | See [GoDoc](https://pkg.go.dev/github.com/jedisct1/openapi-mcp/pkg/openapi2mcp) for complete API documentation. 116 | 117 | ### HTTP Client Development 118 | 119 | When using HTTP mode, openapi-mcp now serves a StreamableHTTP-based MCP server by default. For developers building HTTP clients, you can interact with the `/mcp` endpoint using POST/GET/DELETE as per the StreamableHTTP protocol. SSE is still available by running with the `--http-transport=sse` flag or using `ServeHTTP` in Go. 120 | 121 | See the [StreamableHTTP specification](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) for protocol details. 122 | 123 | If you need SSE, you can still use: 124 | 125 | ```go 126 | // Serve over HTTP using SSE 127 | if err := openapi2mcp.ServeHTTP(srv, ":8080", "/mcp"); err != nil { 128 | log.Fatal(err) 129 | } 130 | ``` 131 | -------------------------------------------------------------------------------- /pkg/openapi2mcp/http_lint.go: -------------------------------------------------------------------------------- 1 | // http_lint.go 2 | package openapi2mcp 3 | 4 | import ( 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | // HTTPLintServer provides HTTP endpoints for OpenAPI validation and linting 13 | type HTTPLintServer struct { 14 | detailedSuggestions bool 15 | } 16 | 17 | // NewHTTPLintServer creates a new HTTP lint server 18 | func NewHTTPLintServer(detailedSuggestions bool) *HTTPLintServer { 19 | return &HTTPLintServer{ 20 | detailedSuggestions: detailedSuggestions, 21 | } 22 | } 23 | 24 | // setCORSAndCacheHeaders sets CORS and caching headers for API responses 25 | func setCORSAndCacheHeaders(w http.ResponseWriter) { 26 | // CORS headers - allow access from any origin 27 | w.Header().Set("Access-Control-Allow-Origin", "*") 28 | w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") 29 | w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept, Authorization") 30 | w.Header().Set("Access-Control-Expose-Headers", "Content-Type") 31 | w.Header().Set("Access-Control-Max-Age", "86400") // 24 hours for preflight cache 32 | 33 | // Caching headers - prevent caching of API responses since they depend on request body 34 | w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") 35 | w.Header().Set("Pragma", "no-cache") 36 | w.Header().Set("Expires", "0") 37 | } 38 | 39 | // HandleLint handles POST requests to lint OpenAPI specs 40 | func (s *HTTPLintServer) HandleLint(w http.ResponseWriter, r *http.Request) { 41 | // Set CORS and caching headers for all responses 42 | setCORSAndCacheHeaders(w) 43 | 44 | // Handle preflight OPTIONS requests 45 | if r.Method == http.MethodOptions { 46 | w.WriteHeader(http.StatusOK) 47 | return 48 | } 49 | 50 | if r.Method != http.MethodPost { 51 | w.Header().Set("Content-Type", "application/json") 52 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 53 | return 54 | } 55 | 56 | w.Header().Set("Content-Type", "application/json") 57 | 58 | var req HTTPLintRequest 59 | if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 60 | http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest) 61 | return 62 | } 63 | 64 | if req.OpenAPISpec == "" { 65 | http.Error(w, "Missing openapi_spec field", http.StatusBadRequest) 66 | return 67 | } 68 | 69 | // Parse the OpenAPI spec 70 | doc, err := LoadOpenAPISpecFromString(req.OpenAPISpec) 71 | if err != nil { 72 | result := &LintResult{ 73 | Success: false, 74 | ErrorCount: 1, 75 | WarningCount: 0, 76 | Issues: []LintIssue{{ 77 | Type: "error", 78 | Message: fmt.Sprintf("Failed to parse OpenAPI spec: %v", err), 79 | Suggestion: "Ensure the OpenAPI spec is valid YAML or JSON and follows OpenAPI 3.x format.", 80 | }}, 81 | Summary: "OpenAPI spec parsing failed.", 82 | } 83 | w.WriteHeader(http.StatusBadRequest) 84 | json.NewEncoder(w).Encode(result) 85 | return 86 | } 87 | 88 | // Perform linting 89 | result := LintOpenAPISpec(doc, s.detailedSuggestions) 90 | 91 | // Set appropriate HTTP status code 92 | if result.Success { 93 | w.WriteHeader(http.StatusOK) 94 | } else { 95 | w.WriteHeader(http.StatusUnprocessableEntity) 96 | } 97 | 98 | json.NewEncoder(w).Encode(result) 99 | } 100 | 101 | // HandleHealth handles GET requests for health checks 102 | func (s *HTTPLintServer) HandleHealth(w http.ResponseWriter, r *http.Request) { 103 | // Set CORS and caching headers 104 | setCORSAndCacheHeaders(w) 105 | 106 | // Handle preflight OPTIONS requests 107 | if r.Method == http.MethodOptions { 108 | w.WriteHeader(http.StatusOK) 109 | return 110 | } 111 | 112 | if r.Method != http.MethodGet { 113 | w.Header().Set("Content-Type", "application/json") 114 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 115 | return 116 | } 117 | 118 | w.Header().Set("Content-Type", "application/json") 119 | response := map[string]interface{}{ 120 | "status": "healthy", 121 | "timestamp": time.Now().UTC().Format(time.RFC3339), 122 | "service": "openapi-lint", 123 | "detailed": s.detailedSuggestions, 124 | } 125 | json.NewEncoder(w).Encode(response) 126 | } 127 | 128 | // ServeHTTPLint starts an HTTP server for linting OpenAPI specs 129 | func ServeHTTPLint(addr string, detailedSuggestions bool) error { 130 | server := NewHTTPLintServer(detailedSuggestions) 131 | 132 | mux := http.NewServeMux() 133 | // Always register both endpoints with different behaviors 134 | validateServer := NewHTTPLintServer(false) // Basic validation 135 | lintServer := NewHTTPLintServer(true) // Detailed linting 136 | 137 | mux.HandleFunc("/validate", validateServer.HandleLint) 138 | mux.HandleFunc("/lint", lintServer.HandleLint) 139 | mux.HandleFunc("/health", server.HandleHealth) 140 | 141 | // Add a root handler that shows available endpoints 142 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 143 | if r.URL.Path != "/" { 144 | http.NotFound(w, r) 145 | return 146 | } 147 | 148 | // Set CORS and caching headers 149 | setCORSAndCacheHeaders(w) 150 | 151 | // Handle preflight OPTIONS requests 152 | if r.Method == http.MethodOptions { 153 | w.WriteHeader(http.StatusOK) 154 | return 155 | } 156 | 157 | w.Header().Set("Content-Type", "application/json") 158 | endpoints := map[string]interface{}{ 159 | "service": "openapi-lint", 160 | "endpoints": map[string]interface{}{}, 161 | "usage": map[string]interface{}{ 162 | "request_body": map[string]string{ 163 | "openapi_spec": "OpenAPI spec as YAML or JSON string", 164 | }, 165 | "response": map[string]interface{}{ 166 | "success": "boolean - whether linting passed", 167 | "error_count": "number - count of errors found", 168 | "warning_count": "number - count of warnings found", 169 | "issues": "array - list of issues with details", 170 | "summary": "string - summary message", 171 | }, 172 | }, 173 | } 174 | 175 | endpointsMap := endpoints["endpoints"].(map[string]interface{}) 176 | // Both endpoints are always available 177 | endpointsMap["POST /validate"] = "Basic OpenAPI validation for critical issues" 178 | endpointsMap["POST /lint"] = "Comprehensive OpenAPI linting with detailed suggestions" 179 | endpointsMap["GET /health"] = "Health check endpoint" 180 | 181 | json.NewEncoder(w).Encode(endpoints) 182 | }) 183 | 184 | log.Printf("Starting OpenAPI validation/linting HTTP server on %s (validate & lint endpoints available)", addr) 185 | 186 | return http.ListenAndServe(addr, mux) 187 | } 188 | -------------------------------------------------------------------------------- /pkg/openapi2mcp/openapi2mcp.go: -------------------------------------------------------------------------------- 1 | // Package openapi2mcp provides functions to expose OpenAPI operations as MCP tools and servers. 2 | // It enables loading OpenAPI specs, generating MCP tool schemas, and running MCP servers that proxy real HTTP calls. 3 | package openapi2mcp 4 | 5 | import ( 6 | "github.com/getkin/kin-openapi/openapi3" 7 | ) 8 | 9 | // OpenAPIOperation describes a single OpenAPI operation to be mapped to an MCP tool. 10 | // It includes the operation's ID, summary, description, HTTP path/method, parameters, request body, and tags. 11 | type OpenAPIOperation struct { 12 | OperationID string 13 | Summary string 14 | Description string 15 | Path string 16 | Method string 17 | Parameters openapi3.Parameters 18 | RequestBody *openapi3.RequestBodyRef 19 | Tags []string 20 | Security openapi3.SecurityRequirements 21 | } 22 | 23 | // ToolGenOptions controls tool generation and output for OpenAPI-MCP conversion. 24 | // 25 | // NameFormat: function to format tool names (e.g., strings.ToLower) 26 | // TagFilter: only include operations with at least one of these tags (if non-empty) 27 | // DryRun: if true, only print the generated tool schemas, don't register 28 | // PrettyPrint: if true, pretty-print the output 29 | // Version: version string to embed in tool annotations 30 | // PostProcessSchema: optional hook to modify each tool's input schema before registration/output 31 | // ConfirmDangerousActions: if true (default), require confirmation for PUT/POST/DELETE tools 32 | // 33 | // func(toolName string, schema map[string]any) map[string]any 34 | type ToolGenOptions struct { 35 | NameFormat func(string) string 36 | TagFilter []string 37 | DryRun bool 38 | PrettyPrint bool 39 | Version string 40 | PostProcessSchema func(toolName string, schema map[string]any) map[string]any 41 | ConfirmDangerousActions bool // if true, add confirmation prompt for dangerous actions 42 | } 43 | -------------------------------------------------------------------------------- /pkg/openapi2mcp/schema.go: -------------------------------------------------------------------------------- 1 | // schema.go 2 | package openapi2mcp 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | 8 | "github.com/getkin/kin-openapi/openapi3" 9 | ) 10 | 11 | // extractProperty recursively extracts a property schema from an OpenAPI SchemaRef. 12 | // Handles allOf, oneOf, anyOf, discriminator, default, example, and basic OpenAPI 3.1 features. 13 | func extractProperty(s *openapi3.SchemaRef) map[string]any { 14 | if s == nil || s.Value == nil { 15 | return nil 16 | } 17 | val := s.Value 18 | prop := map[string]any{} 19 | // Handle allOf (merge all subschemas) 20 | if len(val.AllOf) > 0 { 21 | merged := map[string]any{} 22 | for _, sub := range val.AllOf { 23 | subProp := extractProperty(sub) 24 | for k, v := range subProp { 25 | merged[k] = v 26 | } 27 | } 28 | for k, v := range merged { 29 | prop[k] = v 30 | } 31 | } 32 | // Handle oneOf/anyOf (just include as-is for now) 33 | if len(val.OneOf) > 0 { 34 | fmt.Fprintf(os.Stderr, "[WARN] oneOf used in schema at %p. Only basic support is provided.\n", val) 35 | oneOf := []any{} 36 | for _, sub := range val.OneOf { 37 | oneOf = append(oneOf, extractProperty(sub)) 38 | } 39 | prop["oneOf"] = oneOf 40 | } 41 | if len(val.AnyOf) > 0 { 42 | fmt.Fprintf(os.Stderr, "[WARN] anyOf used in schema at %p. Only basic support is provided.\n", val) 43 | anyOf := []any{} 44 | for _, sub := range val.AnyOf { 45 | anyOf = append(anyOf, extractProperty(sub)) 46 | } 47 | prop["anyOf"] = anyOf 48 | } 49 | // Handle discriminator (OpenAPI 3.0/3.1) 50 | if val.Discriminator != nil { 51 | fmt.Fprintf(os.Stderr, "[WARN] discriminator used in schema at %p. Only basic support is provided.\n", val) 52 | prop["discriminator"] = val.Discriminator 53 | } 54 | // Type, format, description, enum, default, example 55 | if val.Type != nil && len(*val.Type) > 0 { 56 | // Use the first type if multiple types are specified 57 | prop["type"] = (*val.Type)[0] 58 | } 59 | if val.Format != "" { 60 | prop["format"] = val.Format 61 | } 62 | if val.Description != "" { 63 | prop["description"] = val.Description 64 | } 65 | if len(val.Enum) > 0 { 66 | prop["enum"] = val.Enum 67 | } 68 | if val.Default != nil { 69 | prop["default"] = val.Default 70 | } 71 | if val.Example != nil { 72 | prop["example"] = val.Example 73 | } 74 | // Object properties 75 | if val.Type != nil && val.Type.Is("object") && val.Properties != nil { 76 | objProps := map[string]any{} 77 | for name, sub := range val.Properties { 78 | objProps[name] = extractProperty(sub) 79 | } 80 | prop["properties"] = objProps 81 | if len(val.Required) > 0 { 82 | prop["required"] = val.Required 83 | } 84 | } 85 | // Array items 86 | if val.Type != nil && val.Type.Is("array") && val.Items != nil { 87 | prop["items"] = extractProperty(val.Items) 88 | } 89 | return prop 90 | } 91 | 92 | // BuildInputSchema converts OpenAPI parameters and request body schema to a single JSON Schema object for MCP tool input validation. 93 | // Returns a JSON Schema as a map[string]any. 94 | // Example usage for BuildInputSchema: 95 | // 96 | // params := ... // openapi3.Parameters from an operation 97 | // reqBody := ... // *openapi3.RequestBodyRef from an operation 98 | // schema := openapi2mcp.BuildInputSchema(params, reqBody) 99 | // // schema is a map[string]any representing the JSON schema for tool input 100 | func BuildInputSchema(params openapi3.Parameters, requestBody *openapi3.RequestBodyRef) map[string]any { 101 | schema := map[string]any{ 102 | "type": "object", 103 | "properties": map[string]any{}, 104 | } 105 | properties := schema["properties"].(map[string]any) 106 | var required []string 107 | 108 | // Parameters (query, path, header, cookie) 109 | for _, paramRef := range params { 110 | if paramRef == nil || paramRef.Value == nil { 111 | continue 112 | } 113 | p := paramRef.Value 114 | if p.Schema != nil && p.Schema.Value != nil { 115 | if p.Schema.Value.Type != nil && p.Schema.Value.Type.Is("string") && p.Schema.Value.Format == "binary" { 116 | fmt.Fprintf(os.Stderr, "[WARN] Parameter '%s' uses 'string' with 'binary' format. Non-JSON body types are not fully supported.\n", p.Name) 117 | } 118 | prop := extractProperty(p.Schema) 119 | if p.Description != "" { 120 | prop["description"] = p.Description 121 | } 122 | properties[p.Name] = prop 123 | if p.Required { 124 | required = append(required, p.Name) 125 | } 126 | } 127 | // Warn about unsupported parameter locations 128 | if p.In != "query" && p.In != "path" && p.In != "header" && p.In != "cookie" { 129 | fmt.Fprintf(os.Stderr, "[WARN] Parameter '%s' uses unsupported location '%s'.\n", p.Name, p.In) 130 | } 131 | } 132 | 133 | // Request body (only application/json for now) 134 | if requestBody != nil && requestBody.Value != nil { 135 | for mtName := range requestBody.Value.Content { 136 | if mtName != "application/json" { 137 | fmt.Fprintf(os.Stderr, "[WARN] Request body uses media type '%s'. Only 'application/json' is fully supported.\n", mtName) 138 | } 139 | } 140 | if mt := requestBody.Value.Content.Get("application/json"); mt != nil && mt.Schema != nil && mt.Schema.Value != nil { 141 | bodyProp := extractProperty(mt.Schema) 142 | bodyProp["description"] = "The JSON request body." 143 | properties["requestBody"] = bodyProp 144 | if requestBody.Value.Required { 145 | required = append(required, "requestBody") 146 | } 147 | } 148 | } 149 | 150 | if len(required) > 0 { 151 | schema["required"] = required 152 | } 153 | return schema 154 | } 155 | -------------------------------------------------------------------------------- /pkg/openapi2mcp/schema_test.go: -------------------------------------------------------------------------------- 1 | package openapi2mcp 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/getkin/kin-openapi/openapi3" 7 | ) 8 | 9 | func TestSchemaBasic(t *testing.T) { 10 | // TODO: Add tests for schema parsing and validation 11 | t.Run("dummy", func(t *testing.T) { 12 | t.Log("basic schema test placeholder") 13 | }) 14 | } 15 | 16 | func TestBuildInputSchema_Basic(t *testing.T) { 17 | params := openapi3.Parameters{ 18 | &openapi3.ParameterRef{Value: &openapi3.Parameter{ 19 | Name: "foo", 20 | In: "query", 21 | Required: true, 22 | Schema: &openapi3.SchemaRef{Value: &openapi3.Schema{Type: typesPtr("string")}}, 23 | }}, 24 | } 25 | schema := BuildInputSchema(params, nil) 26 | props, _ := schema["properties"].(map[string]any) 27 | if _, ok := props["foo"]; !ok { 28 | t.Fatalf("expected property 'foo' in schema") 29 | } 30 | if req, ok := schema["required"].([]string); !ok || len(req) != 1 || req[0] != "foo" { 31 | t.Fatalf("expected 'foo' to be required, got: %v", schema["required"]) 32 | } 33 | } 34 | 35 | func TestBuildInputSchema_Empty(t *testing.T) { 36 | schema := BuildInputSchema(nil, nil) 37 | if props, ok := schema["properties"].(map[string]any); !ok || len(props) != 0 { 38 | t.Fatalf("expected empty properties, got: %v", props) 39 | } 40 | } 41 | 42 | func TestBuildInputSchema_Malformed(t *testing.T) { 43 | params := openapi3.Parameters{ 44 | &openapi3.ParameterRef{Value: nil}, // malformed 45 | } 46 | schema := BuildInputSchema(params, nil) 47 | if props, ok := schema["properties"].(map[string]any); !ok || len(props) != 0 { 48 | t.Fatalf("expected empty properties for malformed param, got: %v", props) 49 | } 50 | } 51 | 52 | func TestBuildInputSchema_RequiredFromBody(t *testing.T) { 53 | body := &openapi3.RequestBodyRef{Value: &openapi3.RequestBody{ 54 | Required: true, 55 | Content: openapi3.Content{ 56 | "application/json": &openapi3.MediaType{ 57 | Schema: &openapi3.SchemaRef{Value: &openapi3.Schema{ 58 | Type: typesPtr("object"), 59 | Properties: map[string]*openapi3.SchemaRef{ 60 | "bar": {Value: &openapi3.Schema{Type: typesPtr("integer")}}, 61 | }, 62 | Required: []string{"bar"}, 63 | }}, 64 | }, 65 | }, 66 | }} 67 | schema := BuildInputSchema(nil, body) 68 | props, _ := schema["properties"].(map[string]any) 69 | reqBody, ok := props["requestBody"].(map[string]any) 70 | if !ok { 71 | t.Fatalf("expected property 'requestBody' in schema") 72 | } 73 | reqBodyProps, _ := reqBody["properties"].(map[string]any) 74 | if _, ok := reqBodyProps["bar"]; !ok { 75 | t.Fatalf("expected property 'bar' in requestBody schema") 76 | } 77 | if req, ok := reqBody["required"].([]string); !ok || len(req) != 1 || req[0] != "bar" { 78 | t.Fatalf("expected 'bar' to be required in requestBody, got: %v", reqBody["required"]) 79 | } 80 | if req, ok := schema["required"].([]string); !ok || len(req) != 1 || req[0] != "requestBody" { 81 | t.Fatalf("expected 'requestBody' to be required, got: %v", schema["required"]) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /pkg/openapi2mcp/selftest_test.go: -------------------------------------------------------------------------------- 1 | package openapi2mcp 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSelfTestBasic(t *testing.T) { 8 | // TODO: Add tests for self-test logic 9 | t.Run("dummy", func(t *testing.T) { 10 | t.Log("basic selftest placeholder") 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /pkg/openapi2mcp/server.go: -------------------------------------------------------------------------------- 1 | // server.go 2 | package openapi2mcp 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "strings" 10 | 11 | "github.com/getkin/kin-openapi/openapi3" 12 | mcpserver "github.com/jedisct1/openapi-mcp/pkg/mcp/server" 13 | ) 14 | 15 | // authContextFunc extracts authentication headers from HTTP requests and sets them 16 | // as environment variables for the duration of each request. This allows API keys 17 | // and other authentication to be provided via HTTP headers when using HTTP mode. 18 | func authContextFunc(ctx context.Context, r *http.Request) context.Context { 19 | // Save original environment values to restore them later 20 | origAPIKey := os.Getenv("API_KEY") 21 | origBearerToken := os.Getenv("BEARER_TOKEN") 22 | origBasicAuth := os.Getenv("BASIC_AUTH") 23 | 24 | // Extract authentication from HTTP headers 25 | if apiKey := r.Header.Get("X-API-Key"); apiKey != "" { 26 | os.Setenv("API_KEY", apiKey) 27 | } else if apiKey := r.Header.Get("Api-Key"); apiKey != "" { 28 | os.Setenv("API_KEY", apiKey) 29 | } 30 | 31 | if bearerToken := r.Header.Get("Authorization"); bearerToken != "" { 32 | if len(bearerToken) > 7 && bearerToken[:7] == "Bearer " { 33 | os.Setenv("BEARER_TOKEN", bearerToken[7:]) 34 | } else if len(bearerToken) > 6 && bearerToken[:6] == "Basic " { 35 | os.Setenv("BASIC_AUTH", bearerToken[6:]) 36 | } 37 | } 38 | 39 | // Create a context that restores the original environment when done 40 | return &authContext{ 41 | Context: ctx, 42 | origAPIKey: origAPIKey, 43 | origBearerToken: origBearerToken, 44 | origBasicAuth: origBasicAuth, 45 | } 46 | } 47 | 48 | // authContext wraps a context and restores original environment variables when done 49 | type authContext struct { 50 | context.Context 51 | origAPIKey string 52 | origBearerToken string 53 | origBasicAuth string 54 | } 55 | 56 | // Done restores the original environment variables when the context is done 57 | func (c *authContext) Done() <-chan struct{} { 58 | done := c.Context.Done() 59 | if done != nil { 60 | go func() { 61 | <-done 62 | c.restoreEnv() 63 | }() 64 | } 65 | return done 66 | } 67 | 68 | func (c *authContext) restoreEnv() { 69 | if c.origAPIKey != "" { 70 | os.Setenv("API_KEY", c.origAPIKey) 71 | } else { 72 | os.Unsetenv("API_KEY") 73 | } 74 | if c.origBearerToken != "" { 75 | os.Setenv("BEARER_TOKEN", c.origBearerToken) 76 | } else { 77 | os.Unsetenv("BEARER_TOKEN") 78 | } 79 | if c.origBasicAuth != "" { 80 | os.Setenv("BASIC_AUTH", c.origBasicAuth) 81 | } else { 82 | os.Unsetenv("BASIC_AUTH") 83 | } 84 | } 85 | 86 | // NewServer creates a new MCP server, registers all OpenAPI tools, and returns the server. 87 | // Equivalent to calling RegisterOpenAPITools with all operations from the spec. 88 | // Example usage for NewServer: 89 | // 90 | // doc, _ := openapi2mcp.LoadOpenAPISpec("petstore.yaml") 91 | // srv := openapi2mcp.NewServer("petstore", doc.Info.Version, doc) 92 | // openapi2mcp.ServeHTTP(srv, ":8080") 93 | func NewServer(name, version string, doc *openapi3.T) *mcpserver.MCPServer { 94 | ops := ExtractOpenAPIOperations(doc) 95 | srv := mcpserver.NewMCPServer(name, version) 96 | RegisterOpenAPITools(srv, ops, doc, nil) 97 | return srv 98 | } 99 | 100 | // NewServerWithOps creates a new MCP server, registers the provided OpenAPI operations, and returns the server. 101 | // Example usage for NewServerWithOps: 102 | // 103 | // doc, _ := openapi2mcp.LoadOpenAPISpec("petstore.yaml") 104 | // ops := openapi2mcp.ExtractOpenAPIOperations(doc) 105 | // srv := openapi2mcp.NewServerWithOps("petstore", doc.Info.Version, doc, ops) 106 | // openapi2mcp.ServeHTTP(srv, ":8080") 107 | func NewServerWithOps(name, version string, doc *openapi3.T, ops []OpenAPIOperation) *mcpserver.MCPServer { 108 | srv := mcpserver.NewMCPServer(name, version) 109 | RegisterOpenAPITools(srv, ops, doc, nil) 110 | return srv 111 | } 112 | 113 | // ServeStdio starts the MCP server using stdio (wraps mcpserver.ServeStdio). 114 | // Returns an error if the server fails to start. 115 | // Example usage for ServeStdio: 116 | // 117 | // openapi2mcp.ServeStdio(srv) 118 | func ServeStdio(server *mcpserver.MCPServer) error { 119 | return mcpserver.ServeStdio(server) 120 | } 121 | 122 | // ServeHTTP starts the MCP server using HTTP SSE (wraps mcpserver.NewSSEServer and Start). 123 | // addr is the address to listen on, e.g. ":8080". 124 | // basePath is the base HTTP path to mount the MCP server (e.g. "/mcp"). 125 | // Returns an error if the server fails to start. 126 | // Example usage for ServeHTTP: 127 | // 128 | // srv, _ := openapi2mcp.NewServer("petstore", "1.0.0", doc) 129 | // openapi2mcp.ServeHTTP(srv, ":8080", "/custom-base") 130 | func ServeHTTP(server *mcpserver.MCPServer, addr string, basePath string) error { 131 | // Convert the authContextFunc to SSEContextFunc signature 132 | sseAuthContextFunc := func(ctx context.Context, r *http.Request) context.Context { 133 | return authContextFunc(ctx, r) 134 | } 135 | 136 | if basePath == "" { 137 | basePath = "/mcp" 138 | } 139 | 140 | sseServer := mcpserver.NewSSEServer(server, 141 | mcpserver.WithSSEContextFunc(sseAuthContextFunc), 142 | mcpserver.WithStaticBasePath(basePath), 143 | mcpserver.WithSSEEndpoint("/sse"), 144 | mcpserver.WithMessageEndpoint("/message")) 145 | return sseServer.Start(addr) 146 | } 147 | 148 | // GetSSEURL returns the URL for establishing an SSE connection to the MCP server. 149 | // addr is the address the server is listening on (e.g., ":8080", "0.0.0.0:8080", "localhost:8080"). 150 | // basePath is the base HTTP path (e.g., "/mcp"). 151 | // Example usage: 152 | // 153 | // url := openapi2mcp.GetSSEURL(":8080", "/custom-base") 154 | // // Returns: "http://localhost:8080/custom-base/sse" 155 | func GetSSEURL(addr, basePath string) string { 156 | if basePath == "" { 157 | basePath = "/mcp" 158 | } 159 | host := normalizeAddrToHost(addr) 160 | return "http://" + host + basePath + "/sse" 161 | } 162 | 163 | // GetMessageURL returns the URL for sending JSON-RPC requests to the MCP server. 164 | // addr is the address the server is listening on (e.g., ":8080", "0.0.0.0:8080", "localhost:8080"). 165 | // basePath is the base HTTP path (e.g., "/mcp"). 166 | // sessionID should be the session ID received from the SSE endpoint event. 167 | // Example usage: 168 | // 169 | // url := openapi2mcp.GetMessageURL(":8080", "/custom-base", "session-id-123") 170 | // // Returns: "http://localhost:8080/custom-base/message?sessionId=session-id-123" 171 | func GetMessageURL(addr, basePath, sessionID string) string { 172 | if basePath == "" { 173 | basePath = "/mcp" 174 | } 175 | host := normalizeAddrToHost(addr) 176 | return fmt.Sprintf("http://%s%s/message?sessionId=%s", host, basePath, sessionID) 177 | } 178 | 179 | // GetStreamableHTTPURL returns the URL for the Streamable HTTP endpoint of the MCP server. 180 | // addr is the address the server is listening on (e.g., ":8080", "0.0.0.0:8080", "localhost:8080"). 181 | // basePath is the base HTTP path (e.g., "/mcp"). 182 | // Example usage: 183 | // 184 | // url := openapi2mcp.GetStreamableHTTPURL(":8080", "/custom-base") 185 | // // Returns: "http://localhost:8080/custom-base" 186 | func GetStreamableHTTPURL(addr, basePath string) string { 187 | if basePath == "" { 188 | basePath = "/mcp" 189 | } 190 | host := normalizeAddrToHost(addr) 191 | return "http://" + host + basePath 192 | } 193 | 194 | // normalizeAddrToHost converts an addr (as used by net/http) to a host:port string suitable for URLs. 195 | // If addr is just ":8080", returns "localhost:8080". If it already includes a host, returns as is. 196 | func normalizeAddrToHost(addr string) string { 197 | addr = strings.TrimSpace(addr) 198 | if addr == "" { 199 | return "localhost" 200 | } 201 | if strings.HasPrefix(addr, ":") { 202 | return "localhost" + addr 203 | } 204 | return addr 205 | } 206 | 207 | // HandlerForBasePath returns an http.Handler that serves the given MCP server at the specified basePath. 208 | // This is useful for multi-mount HTTP servers, where you want to serve multiple OpenAPI schemas at different URL paths. 209 | // Example usage: 210 | // 211 | // handler := openapi2mcp.HandlerForBasePath(srv, "/petstore") 212 | // mux.Handle("/petstore/", handler) 213 | func HandlerForBasePath(server *mcpserver.MCPServer, basePath string) http.Handler { 214 | sseAuthContextFunc := func(ctx context.Context, r *http.Request) context.Context { 215 | return authContextFunc(ctx, r) 216 | } 217 | if basePath == "" { 218 | basePath = "/mcp" 219 | } 220 | sseServer := mcpserver.NewSSEServer(server, 221 | mcpserver.WithSSEContextFunc(sseAuthContextFunc), 222 | mcpserver.WithStaticBasePath(basePath), 223 | mcpserver.WithSSEEndpoint("/sse"), 224 | mcpserver.WithMessageEndpoint("/message"), 225 | ) 226 | return sseServer 227 | } 228 | 229 | // ServeStreamableHTTP starts the MCP server using HTTP StreamableHTTP (wraps mcpserver.NewStreamableHTTPServer and Start). 230 | // addr is the address to listen on, e.g. ":8080". 231 | // basePath is the base HTTP path to mount the MCP server (e.g. "/mcp"). 232 | // Returns an error if the server fails to start. 233 | // Example usage for ServeStreamableHTTP: 234 | // 235 | // srv, _ := openapi2mcp.NewServer("petstore", "1.0.0", doc) 236 | // openapi2mcp.ServeStreamableHTTP(srv, ":8080", "/custom-base") 237 | func ServeStreamableHTTP(server *mcpserver.MCPServer, addr string, basePath string) error { 238 | streamableAuthContextFunc := func(ctx context.Context, r *http.Request) context.Context { 239 | return authContextFunc(ctx, r) 240 | } 241 | 242 | if basePath == "" { 243 | basePath = "/mcp" 244 | } 245 | 246 | streamableServer := mcpserver.NewStreamableHTTPServer(server, 247 | mcpserver.WithHTTPContextFunc(streamableAuthContextFunc), 248 | mcpserver.WithEndpointPath(basePath), 249 | ) 250 | return streamableServer.Start(addr) 251 | } 252 | 253 | // HandlerForStreamableHTTP returns an http.Handler that serves the given MCP server at the specified basePath using StreamableHTTP. 254 | // This is useful for multi-mount HTTP servers, where you want to serve multiple OpenAPI schemas at different URL paths. 255 | // Example usage: 256 | // 257 | // handler := openapi2mcp.HandlerForStreamableHTTP(srv, "/petstore") 258 | // mux.Handle("/petstore", handler) 259 | func HandlerForStreamableHTTP(server *mcpserver.MCPServer, basePath string) http.Handler { 260 | streamableAuthContextFunc := func(ctx context.Context, r *http.Request) context.Context { 261 | return authContextFunc(ctx, r) 262 | } 263 | if basePath == "" { 264 | basePath = "/mcp" 265 | } 266 | streamableServer := mcpserver.NewStreamableHTTPServer(server, 267 | mcpserver.WithHTTPContextFunc(streamableAuthContextFunc), 268 | mcpserver.WithEndpointPath(basePath), 269 | ) 270 | return streamableServer 271 | } 272 | -------------------------------------------------------------------------------- /pkg/openapi2mcp/server_test.go: -------------------------------------------------------------------------------- 1 | package openapi2mcp 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGetSSEURL(t *testing.T) { 8 | tests := []struct { 9 | name string 10 | addr string 11 | basePath string 12 | expected string 13 | }{ 14 | { 15 | name: "basic addr", 16 | addr: ":8080", 17 | basePath: "/mcp", 18 | expected: "http://localhost:8080/mcp/sse", 19 | }, 20 | { 21 | name: "addr with host", 22 | addr: "127.0.0.1:3000", 23 | basePath: "/api", 24 | expected: "http://127.0.0.1:3000/api/sse", 25 | }, 26 | { 27 | name: "addr with hostname", 28 | addr: "myhost:9000", 29 | basePath: "/foo", 30 | expected: "http://myhost:9000/foo/sse", 31 | }, 32 | { 33 | name: "empty basePath", 34 | addr: ":8080", 35 | basePath: "", 36 | expected: "http://localhost:8080/mcp/sse", 37 | }, 38 | } 39 | 40 | for _, tt := range tests { 41 | t.Run(tt.name, func(t *testing.T) { 42 | result := GetSSEURL(tt.addr, tt.basePath) 43 | if result != tt.expected { 44 | t.Errorf("GetSSEURL(%q, %q) = %q, want %q", tt.addr, tt.basePath, result, tt.expected) 45 | } 46 | }) 47 | } 48 | } 49 | 50 | func TestGetMessageURL(t *testing.T) { 51 | tests := []struct { 52 | name string 53 | addr string 54 | basePath string 55 | sessionID string 56 | expected string 57 | }{ 58 | { 59 | name: "basic addr with session", 60 | addr: ":8080", 61 | basePath: "/mcp", 62 | sessionID: "session-123", 63 | expected: "http://localhost:8080/mcp/message?sessionId=session-123", 64 | }, 65 | { 66 | name: "addr with host", 67 | addr: "127.0.0.1:3000", 68 | basePath: "/api", 69 | sessionID: "abc-def-ghi", 70 | expected: "http://127.0.0.1:3000/api/message?sessionId=abc-def-ghi", 71 | }, 72 | { 73 | name: "hostname and uuid session", 74 | addr: "myhost:9000", 75 | basePath: "/foo", 76 | sessionID: "550e8400-e29b-41d4-a716-446655440000", 77 | expected: "http://myhost:9000/foo/message?sessionId=550e8400-e29b-41d4-a716-446655440000", 78 | }, 79 | { 80 | name: "empty session ID", 81 | addr: ":8080", 82 | basePath: "/mcp", 83 | sessionID: "", 84 | expected: "http://localhost:8080/mcp/message?sessionId=", 85 | }, 86 | } 87 | 88 | for _, tt := range tests { 89 | t.Run(tt.name, func(t *testing.T) { 90 | result := GetMessageURL(tt.addr, tt.basePath, tt.sessionID) 91 | if result != tt.expected { 92 | t.Errorf("GetMessageURL(%q, %q, %q) = %q, want %q", tt.addr, tt.basePath, tt.sessionID, result, tt.expected) 93 | } 94 | }) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /pkg/openapi2mcp/spec.go: -------------------------------------------------------------------------------- 1 | // spec.go 2 | package openapi2mcp 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "regexp" 8 | 9 | "github.com/getkin/kin-openapi/openapi3" 10 | ) 11 | 12 | // LoadOpenAPISpec loads and parses an OpenAPI YAML or JSON file from the given path. 13 | // Returns the parsed OpenAPI document or an error. 14 | // Example usage for LoadOpenAPISpec: 15 | // 16 | // doc, err := openapi2mcp.LoadOpenAPISpec("petstore.yaml") 17 | // if err != nil { log.Fatal(err) } 18 | // ops := openapi2mcp.ExtractOpenAPIOperations(doc) 19 | func LoadOpenAPISpec(path string) (*openapi3.T, error) { 20 | data, err := os.ReadFile(path) 21 | if err != nil { 22 | return nil, fmt.Errorf("failed to read OpenAPI spec file: %w", err) 23 | } 24 | return LoadOpenAPISpecFromBytes(data) 25 | } 26 | 27 | // LoadOpenAPISpecFromString loads and parses an OpenAPI YAML or JSON spec from a string. 28 | // Returns the parsed OpenAPI document or an error. 29 | func LoadOpenAPISpecFromString(data string) (*openapi3.T, error) { 30 | return LoadOpenAPISpecFromBytes([]byte(data)) 31 | } 32 | 33 | // LoadOpenAPISpecFromBytes loads and parses an OpenAPI YAML or JSON spec from a byte slice. 34 | // Returns the parsed OpenAPI document or an error. 35 | func LoadOpenAPISpecFromBytes(data []byte) (*openapi3.T, error) { 36 | loader := openapi3.NewLoader() 37 | doc, err := loader.LoadFromData(data) 38 | if err != nil { 39 | return nil, fmt.Errorf("failed to parse OpenAPI spec: %w", err) 40 | } 41 | if err := doc.Validate(loader.Context); err != nil { 42 | return nil, fmt.Errorf("OpenAPI spec validation failed: %w", err) 43 | } 44 | return doc, nil 45 | } 46 | 47 | // ExtractOpenAPIOperations extracts all operations from the OpenAPI spec, merging path-level and operation-level parameters. 48 | // Returns a slice of OpenAPIOperation describing each operation. 49 | // Example usage for ExtractOpenAPIOperations: 50 | // 51 | // doc, err := openapi2mcp.LoadOpenAPISpec("petstore.yaml") 52 | // if err != nil { log.Fatal(err) } 53 | // ops := openapi2mcp.ExtractOpenAPIOperations(doc) 54 | func ExtractOpenAPIOperations(doc *openapi3.T) []OpenAPIOperation { 55 | var ops []OpenAPIOperation 56 | for path, pathItem := range doc.Paths.Map() { 57 | for method, op := range pathItem.Operations() { 58 | id := op.OperationID 59 | if id == "" { 60 | id = fmt.Sprintf("%s_%s", method, path) 61 | } 62 | desc := op.Description 63 | 64 | // Merge path-level and operation-level parameters 65 | mergedParams := openapi3.Parameters{} 66 | if pathItem.Parameters != nil { 67 | mergedParams = append(mergedParams, pathItem.Parameters...) 68 | } 69 | if op.Parameters != nil { 70 | mergedParams = append(mergedParams, op.Parameters...) 71 | } 72 | 73 | tags := op.Tags 74 | var security openapi3.SecurityRequirements 75 | if op.Security != nil { 76 | security = *op.Security 77 | } else { 78 | security = doc.Security 79 | } 80 | ops = append(ops, OpenAPIOperation{ 81 | OperationID: id, 82 | Summary: op.Summary, 83 | Description: desc, 84 | Path: path, 85 | Method: method, 86 | Parameters: mergedParams, 87 | RequestBody: op.RequestBody, 88 | Tags: tags, 89 | Security: security, 90 | }) 91 | } 92 | } 93 | return ops 94 | } 95 | 96 | // ExtractFilteredOpenAPIOperations returns only those operations whose description matches includeRegex (if not nil) and does not match excludeRegex (if not nil). 97 | // Returns a filtered slice of OpenAPIOperation. 98 | // Example usage for ExtractFilteredOpenAPIOperations: 99 | // 100 | // include := regexp.MustCompile("pets") 101 | // filtered := openapi2mcp.ExtractFilteredOpenAPIOperations(doc, include, nil) 102 | func ExtractFilteredOpenAPIOperations(doc *openapi3.T, includeRegex, excludeRegex *regexp.Regexp) []OpenAPIOperation { 103 | all := ExtractOpenAPIOperations(doc) 104 | var filtered []OpenAPIOperation 105 | for _, op := range all { 106 | desc := op.Description 107 | if desc == "" { 108 | desc = op.Summary 109 | } 110 | if includeRegex != nil && !includeRegex.MatchString(desc) { 111 | continue 112 | } 113 | if excludeRegex != nil && excludeRegex.MatchString(desc) { 114 | continue 115 | } 116 | filtered = append(filtered, op) 117 | } 118 | return filtered 119 | } 120 | -------------------------------------------------------------------------------- /pkg/openapi2mcp/summary.go: -------------------------------------------------------------------------------- 1 | // summary.go 2 | package openapi2mcp 3 | 4 | import "fmt" 5 | 6 | // PrintToolSummary prints a summary of the generated tools (count, tags, etc). 7 | func PrintToolSummary(ops []OpenAPIOperation) { 8 | tagCount := map[string]int{} 9 | for _, op := range ops { 10 | for _, tag := range op.Tags { 11 | tagCount[tag]++ 12 | } 13 | } 14 | fmt.Printf("Total tools: %d\n", len(ops)) 15 | if len(tagCount) > 0 { 16 | fmt.Println("Tags:") 17 | for tag, count := range tagCount { 18 | fmt.Printf(" %s: %d\n", tag, count) 19 | } 20 | } 21 | } 22 | 23 | // Example usage for PrintToolSummary: 24 | // 25 | // doc, _ := openapi2mcp.LoadOpenAPISpec("petstore.yaml") 26 | // ops := openapi2mcp.ExtractOpenAPIOperations(doc) 27 | // openapi2mcp.PrintToolSummary(ops) 28 | -------------------------------------------------------------------------------- /pkg/openapi2mcp/types.go: -------------------------------------------------------------------------------- 1 | // Package openapi2mcp provides functionality for converting OpenAPI specifications to MCP tools. 2 | // For working with MCP types and tools directly, import github.com/jedisct1/openapi-mcp/pkg/mcp/mcp 3 | // and github.com/jedisct1/openapi-mcp/pkg/mcp/server 4 | package openapi2mcp 5 | 6 | // LintIssue represents a single linting issue found in an OpenAPI spec 7 | type LintIssue struct { 8 | Type string `json:"type"` // "error" or "warning" 9 | Message string `json:"message"` // The main error/warning message 10 | Suggestion string `json:"suggestion"` // Actionable suggestion for fixing the issue 11 | Operation string `json:"operation,omitempty"` // Operation ID where the issue was found 12 | Path string `json:"path,omitempty"` // API path where the issue was found 13 | Method string `json:"method,omitempty"` // HTTP method where the issue was found 14 | Parameter string `json:"parameter,omitempty"` // Parameter name where the issue was found 15 | Field string `json:"field,omitempty"` // Specific field where the issue was found 16 | } 17 | 18 | // LintResult represents the result of linting or validating an OpenAPI spec 19 | type LintResult struct { 20 | Success bool `json:"success"` // Whether the linting/validation passed 21 | ErrorCount int `json:"error_count"` // Number of errors found 22 | WarningCount int `json:"warning_count"` // Number of warnings found 23 | Issues []LintIssue `json:"issues"` // List of all issues found 24 | Summary string `json:"summary,omitempty"` // Summary message 25 | } 26 | 27 | // HTTPLintRequest represents the request body for HTTP lint/validate endpoints 28 | type HTTPLintRequest struct { 29 | OpenAPISpec string `json:"openapi_spec"` // The OpenAPI spec as a YAML or JSON string 30 | } 31 | --------------------------------------------------------------------------------