Skip to content

Commit 1555d8c

Browse files
committed
Release v0.0.2
1 parent 4c3ff4d commit 1555d8c

3 files changed

Lines changed: 92 additions & 46 deletions

File tree

README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
**Generate MCP tool definitions directly from a Swagger/OpenAPI specification file.**
1111

12-
OpenAPI-MCP is a library and command-line tool that reads a `swagger.json` or `openapi.yaml` file and generates a corresponding [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) toolset. This allows MCP-compatible clients like [Cursor](https://cursor.sh/) to interact with APIs described by standard OpenAPI specifications.
12+
OpenAPI-MCP is a dockerized MCP server that reads a `swagger.json` or `openapi.yaml` file and generates a corresponding [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) toolset. This allows MCP-compatible clients like [Cursor](https://cursor.sh/) to interact with APIs described by standard OpenAPI specifications. Now you can enable your AI agent to access any API by simply providing its OpenAPI/Swagger specification - no additional coding required.
1313

1414
## Table of Contents
1515

@@ -22,22 +22,26 @@ OpenAPI-MCP is a library and command-line tool that reads a `swagger.json` or `o
2222
- [Command-Line Options](#command-line-options)
2323
- [Environment Variables](#environment-variables)
2424

25+
## Demo
26+
27+
Run the demo yourself: [Running the Weatherbit Example (Step-by-Step)](#running-the-weatherbit-example-step-by-step)
28+
2529
## Why OpenAPI-MCP?
2630

2731
- **Standard Compliance:** Leverage your existing OpenAPI/Swagger documentation.
2832
- **Automatic Tool Generation:** Create MCP tools without manual configuration for each endpoint.
2933
- **Flexible API Key Handling:** Securely manage API key authentication for the proxied API without exposing keys to the MCP client.
3034
- **Local & Remote Specs:** Works with local specification files or remote URLs.
31-
- **Command-Line Tool:** Easily integrate MCP generation into build or deployment scripts.
35+
- **Dockerized Tool:** Easily deploy and run as a containerized service with Docker.
3236

3337
## Features
3438

3539
- **OpenAPI v2 (Swagger) & v3 Support:** Parses standard specification formats.
3640
- **Schema Generation:** Creates MCP tool schemas from OpenAPI operation parameters and request/response definitions.
3741
- **Secure API Key Management:**
38-
- Injects API keys into requests (`header`, `query`, `path`, `cookie`) based on command-line configuration.
39-
- Loads API keys directly from flags (`--api-key`), environment variables (`--api-key-env`), or `.env` files located alongside local specs.
40-
- Keeps API keys hidden from the end MCP client (e.g., the AI assistant).
42+
- Injects API keys into requests (`header`, `query`, `path`, `cookie`) based on command-line configuration.
43+
- Loads API keys directly from flags (`--api-key`), environment variables (`--api-key-env`), or `.env` files located alongside local specs.
44+
- Keeps API keys hidden from the end MCP client (e.g., the AI assistant).
4145
- **Server URL Detection:** Uses server URLs from the spec as the base for tool interactions (can be overridden).
4246
- **Filtering:** Options to include/exclude specific operations or tags (`--include-tag`, `--exclude-tag`, `--include-op`, `--exclude-op`).
4347
- **Request Header Injection:** Pass custom headers (e.g., for additional auth, tracing) via the `REQUEST_HEADERS` environment variable.

example/agent_demo.png

370 KB
Loading

pkg/server/server_test.go

Lines changed: 83 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ package server
22

33
import (
44
"bytes"
5+
"context"
56
"encoding/json"
67
"fmt"
78
"io"
89
"log"
910
"net/http"
1011
"net/http/httptest"
1112
"strings"
13+
"sync"
1214
"testing"
1315
"time"
1416

@@ -357,6 +359,10 @@ func TestHttpMethodPostHandler(t *testing.T) {
357359
}
358360

359361
func TestHttpMethodGetHandler(t *testing.T) {
362+
// Basic setup
363+
// cfg := &config.Config{} // Use random port - Removed Port field - Removed unused variable
364+
// Setup(cfg, nil) // No toolset needed for GET handler base functionality - Removed Setup call
365+
360366
// --- Test Setup ---
361367
req := httptest.NewRequest(http.MethodGet, "/mcp", nil)
362368
rr := httptest.NewRecorder() // Use ResponseRecorder directly for header checks
@@ -373,63 +379,84 @@ func TestHttpMethodGetHandler(t *testing.T) {
373379
// Restore original connections after test
374380
defer func() {
375381
connMutex.Lock()
376-
activeConnections = originalConnections
382+
// Clean up any connection potentially added by the test run
383+
// (Find the ID from the recorder if available)
384+
connID := rr.Header().Get("X-Connection-ID") // Get ID written by handler
385+
if connID != "" {
386+
if ch, exists := activeConnections[connID]; exists {
387+
close(ch) // Close the channel if it exists
388+
}
389+
}
390+
activeConnections = originalConnections // Restore the original map
377391
connMutex.Unlock()
378392
}()
379393

380394
// --- Execute Handler (in a goroutine as it blocks waiting for context) ---
381-
done := make(chan struct{})
395+
ctx, cancel := context.WithCancel(context.Background())
396+
defer cancel() // Ensure the handler goroutine can eventually exit
397+
req = req.WithContext(ctx)
398+
399+
hwg := sync.WaitGroup{}
400+
hwg.Add(1)
382401
go func() {
383-
defer close(done)
402+
defer hwg.Done()
384403
httpMethodGetHandler(rr, req)
385404
}()
386405

387406
// --- Assertions ---
407+
var connID string
388408

389-
// Wait briefly for headers and initial events to be written
390-
// This is tricky because the handler blocks. We might only be able to check headers reliably.
391-
// Reading the body/events might require a more sophisticated setup or ending the request.
409+
// Wait for the handler to write headers, initial body, and register the connection.
410+
// Use Eventually to poll for multiple conditions to be met atomically before proceeding.
411+
assert.Eventually(t, func() bool {
412+
if rr.Code != http.StatusOK {
413+
return false // Wait for status OK first
414+
}
392415

393-
// Check Headers (before handler potentially blocks indefinitely)
394-
// It seems ResponseRecorder might not capture headers written before blocking?
395-
// Let's try checking status code first, which is written before blocking starts.
416+
// Check headers are set correctly
417+
if rr.Header().Get("Content-Type") != "text/event-stream" ||
418+
rr.Header().Get("Cache-Control") != "no-cache" ||
419+
rr.Header().Get("Connection") != "keep-alive" {
420+
return false
421+
}
422+
connID = rr.Header().Get("X-Connection-ID")
423+
if connID == "" {
424+
return false // Wait for connection ID header
425+
}
396426

397-
// Poll for the status code to be written (indicates headers are likely set)
398-
assert.Eventually(t, func() bool {
399-
return rr.Code == http.StatusOK
400-
}, 100*time.Millisecond, 10*time.Millisecond, "Handler did not write StatusOK")
401-
402-
// If status is OK, check headers
403-
if rr.Code == http.StatusOK {
404-
assert.Equal(t, "text/event-stream", rr.Header().Get("Content-Type"))
405-
assert.Equal(t, "no-cache", rr.Header().Get("Cache-Control"))
406-
assert.Equal(t, "keep-alive", rr.Header().Get("Connection"))
407-
connID := rr.Header().Get("X-Connection-ID")
408-
assert.NotEmpty(t, connID, "X-Connection-ID header should be set")
409-
410-
// Check if connection was added to the map
427+
// Check connection is registered in the map
411428
connMutex.RLock()
412429
_, exists := activeConnections[connID]
413430
connMutex.RUnlock()
414-
assert.True(t, exists, "Connection ID should be added to activeConnections map")
415-
416-
// Check body for initial events (might be flaky due to goroutine/blocking)
417-
// Read the body content written so far
418-
bodyContent := rr.Body.String()
419-
assert.Contains(t, bodyContent, ":ok\n\n", "SSE stream should contain :ok preamble")
420-
assert.Contains(t, bodyContent, "event: endpoint\ndata: /mcp?sessionId="+connID+"\n\n", "SSE stream should contain endpoint event")
421-
assert.Contains(t, bodyContent, "event: message\ndata: {", "SSE stream should contain message event start")
422-
assert.Contains(t, bodyContent, "\"method\":\"mcp-ready\"", "SSE stream should contain mcp-ready message")
423-
assert.Contains(t, bodyContent, "\"connectionId\":\""+connID+"\"", "mcp-ready message should contain correct connectionId")
424-
425-
// Clean up the connection we added
426-
cleanupTestConnection(connID)
427-
} else {
428-
t.Logf("Handler did not return StatusOK, headers might not be set. Code: %d", rr.Code)
429-
}
431+
if !exists {
432+
return false // Wait for connection ID in map
433+
}
430434

431-
// We cannot easily wait for `done` because the handler blocks indefinitely.
432-
// For coverage, triggering the initial part is the main goal here.
435+
// Check initial body content is present
436+
bodyContent := rr.Body.String() // Read body within the check
437+
if !strings.Contains(bodyContent, ":ok\n\n") ||
438+
!strings.Contains(bodyContent, "event: endpoint\ndata: /mcp?sessionId="+connID+"\n\n") ||
439+
!strings.Contains(bodyContent, "event: mcp-ready\ndata: {") || // Check start of ready event
440+
!strings.Contains(bodyContent, `"connectionId":"`+connID+`"}`) { // Check connection ID within ready event
441+
return false
442+
}
443+
444+
return true // All initial conditions met
445+
}, 5*time.Second, 50*time.Millisecond, "Handler did not initialize SSE stream correctly within timeout") // Increased timeout slightly
446+
447+
// If Eventually succeeded, all initial checks passed without races.
448+
// connID will be set based on the header read within the successful Eventually tick.
449+
450+
// No need to call cleanupTestConnection(connID) explicitly here.
451+
// The deferred function above will handle closing the channel and resetting the map.
452+
453+
// Cancel context to allow handler goroutine to exit cleanly
454+
cancel()
455+
456+
// Wait for the handler goroutine to finish (optional but good practice for cleanup)
457+
if !waitTimeout(&hwg, 1*time.Second) {
458+
t.Error("Handler goroutine did not exit cleanly after context cancellation")
459+
}
433460
}
434461

435462
func TestExecuteToolCall(t *testing.T) {
@@ -1046,3 +1073,18 @@ func TestHttpMethodGetHandler_GoroutineErrors(t *testing.T) {
10461073

10471074
// TODO: Add sub-test for Error_on_Ping_Write
10481075
}
1076+
1077+
// Helper function to wait for a WaitGroup with a timeout
1078+
func waitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
1079+
c := make(chan struct{})
1080+
go func() {
1081+
defer close(c)
1082+
wg.Wait()
1083+
}()
1084+
select {
1085+
case <-c:
1086+
return true // Completed normally
1087+
case <-time.After(timeout):
1088+
return false // Timed out
1089+
}
1090+
}

0 commit comments

Comments
 (0)