Compare commits

..

No commits in common. "main" and "v0.0.1" have entirely different histories.
main ... v0.0.1

12 changed files with 133 additions and 359 deletions

View file

@ -51,7 +51,7 @@ else
fi
# Extract current version.
CURRENT_VERSION=$(git tag --sort=-v:refname | head -n 1 | sed 's/^v//' || echo "0.0.0")
CURRENT_VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0")
IFS='.' read -r MAJOR MINOR PATCH <<< "${CURRENT_VERSION}"
# Determine which part to increment

4
.gitignore vendored
View file

@ -36,7 +36,3 @@ coverage.html
# IDE files
.vscode
# node modules
node_modules
openmcpauthproxy

View file

@ -24,9 +24,9 @@ TEST_OPTS := -v -race
.PHONY: all clean test fmt lint vet coverage help
# Default target
all: lint test build-linux build-linux-arm build-darwin build-windows
all: lint test build-linux build-linux-arm build-darwin
build: clean test build-linux build-linux-arm build-darwin build-windows
build: clean test build-linux build-linux-arm build-darwin
build-linux:
mkdir -p $(BUILD_DIR)/linux
@ -46,12 +46,6 @@ build-darwin:
-o $(BUILD_DIR)/darwin/openmcpauthproxy $(PROJECT_ROOT)/cmd/proxy
cp config.yaml $(BUILD_DIR)/darwin
build-windows:
mkdir -p $(BUILD_DIR)/windows
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -x -ldflags "-X main.version=$(BUILD_VERSION)" \
-o $(BUILD_DIR)/windows/openmcpauthproxy.exe ./cmd/proxy
cp config.yaml $(BUILD_DIR)/windows
# Clean build artifacts
clean:
@echo "Cleaning build artifacts..."

106
README.md
View file

@ -51,16 +51,10 @@ Open MCP Auth Proxy sits between MCP clients and your MCP server to:
2. Start the proxy in demo mode (uses pre-configured authentication with Asgardeo sandbox):
#### Linux/macOS:
```bash
./openmcpauthproxy --demo
```
#### Windows:
```powershell
.\openmcpauthproxy.exe --demo
```
> The repository comes with a default `config.yaml` file that contains the basic configuration:
>
> ```yaml
@ -221,102 +215,12 @@ asgardeo:
client_id: "<client_id>"
client_secret: "<client_secret>"
```
## Build from Source
### Prerequisites
* Go 1.20 or higher
* Git
* Make (optional, for simplified builds)
### Clone and Build
1. **Clone the repository:**
```bash
git clone https://github.com/wso2/open-mcp-auth-proxy
cd open-mcp-auth-proxy
```
2. **Install dependencies:**
```bash
go get -v -t -d ./...
```
3. **Build the application:**
**Option A: Using Make**
```bash
# Build for all platforms
make all
# Or build for specific platforms
make build-linux # For Linux (x86_64)
make build-linux-arm # For ARM-based Linux
make build-darwin # For macOS
make build-windows # For Windows
```
**Option B: Manual build (works on all platforms)**
```bash
# Build for your current platform
go build -o openmcpauthproxy ./cmd/proxy
# Cross-compile for other platforms
GOOS=linux GOARCH=amd64 go build -o openmcpauthproxy-linux ./cmd/proxy
GOOS=windows GOARCH=amd64 go build -o openmcpauthproxy.exe ./cmd/proxy
GOOS=darwin GOARCH=amd64 go build -o openmcpauthproxy-macos ./cmd/proxy
```
### Run the Built Application
After building, you'll find the executables in the `build` directory (when using Make) or in your project root (when building manually).
**Linux/macOS:**
```bash
# If built with Make
./build/linux/openmcpauthproxy --demo
# If built manually
./openmcpauthproxy --demo
```
**Windows:**
```powershell
# If built with Make
.\build\windows\openmcpauthproxy.exe --demo
# If built manually
.\openmcpauthproxy.exe --demo
```
### Available Command Line Options
### Build from source
```bash
# Start in demo mode (using Asgardeo sandbox)
./openmcpauthproxy --demo
# Start with your own Asgardeo organization
./openmcpauthproxy --asgardeo
# Use stdio transport mode instead of SSE
./openmcpauthproxy --demo --stdio
# Enable debug logging
./openmcpauthproxy --demo --debug
# Show all available options
./openmcpauthproxy --help
```
### Additional Make Targets
If you're using Make, these additional targets are available:
```bash
make test # Run tests
make coverage # Run tests with coverage report
make fmt # Format code with gofmt
make vet # Run go vet
make clean # Clean build artifacts
make help # Show all available targets
git clone https://github.com/wso2/open-mcp-auth-proxy
cd open-mcp-auth-proxy
go get github.com/golang-jwt/jwt/v4 gopkg.in/yaml.v2
go build -o openmcpauthproxy ./cmd/proxy
```

View file

@ -2,15 +2,14 @@
# Common configuration for all transport modes
listen_port: 8080
base_url: "http://localhost:3001" # Base URL for the MCP server
port: 3001 # Port for the MCP server
base_url: "http://localhost:8000" # Base URL for the MCP server
port: 8000 # Port for the MCP server
timeout_seconds: 10
# Path configuration
paths:
sse: "/sse" # SSE endpoint path
messages: "/messages/" # Messages endpoint path
streamable_http: "/mcp" # MCP endpoint path
# Transport mode configuration
transport_mode: "sse" # Options: "sse" or "stdio"
@ -29,7 +28,7 @@ path_mapping:
# CORS configuration
cors:
allowed_origins:
- "http://127.0.0.1:6274"
- "http://localhost:5173"
allowed_methods:
- "GET"
- "POST"

View file

@ -113,7 +113,6 @@ func (p *asgardeoProvider) RegisterHandler() http.HandlerFunc {
if err := p.createAsgardeoApplication(regReq); err != nil {
logger.Warn("Asgardeo application creation failed: %v", err)
http.Error(w, "Failed to create application in Asgardeo", http.StatusInternalServerError)
// Optionally http.Error(...) if you want to fail
// or continue to return partial data.
}
@ -270,18 +269,6 @@ func buildAsgardeoPayload(regReq RegisterRequest) map[string]interface{} {
}
appName += "-" + randomString(5)
// Build redirect URIs regex from list of redirect URIs : regexp=(https://app.example.com/callback1|https://app.example.com/callback2)
redirectURI := "regexp=(" + strings.Join(regReq.RedirectURIs, "|") + ")"
redirectURIs := []string{redirectURI}
// Filter unsupported grant types
var grantTypes []string
for _, gt := range regReq.GrantTypes {
if gt == "authorization_code" || gt == "refresh_token" {
grantTypes = append(grantTypes, gt)
}
}
return map[string]interface{}{
"name": appName,
"templateId": "custom-application-oidc",
@ -289,10 +276,10 @@ func buildAsgardeoPayload(regReq RegisterRequest) map[string]interface{} {
"oidc": map[string]interface{}{
"clientId": regReq.ClientID,
"clientSecret": regReq.ClientSecret,
"grantTypes": grantTypes,
"callbackURLs": redirectURIs,
"grantTypes": regReq.GrantTypes,
"callbackURLs": regReq.RedirectURIs,
"allowedOrigins": []string{},
"publicClient": true,
"publicClient": false,
"pkce": map[string]bool{
"mandatory": true,
"supportPlainTransformAlgorithm": true,

View file

@ -3,8 +3,6 @@ package config
import (
"fmt"
"os"
"runtime"
"strings"
"gopkg.in/yaml.v2"
)
@ -21,7 +19,6 @@ const (
type PathsConfig struct {
SSE string `yaml:"sse"`
Messages string `yaml:"messages"`
StreamableHTTP string `yaml:"streamable_http"` // Path for streamable HTTP requests
}
// StdioConfig contains stdio-specific configuration
@ -139,7 +136,7 @@ func (c *Config) Validate() error {
// GetMCPPaths returns the list of paths that should be proxied to the MCP server
func (c *Config) GetMCPPaths() []string {
return []string{c.Paths.SSE, c.Paths.Messages, c.Paths.StreamableHTTP}
return []string{c.Paths.SSE, c.Paths.Messages}
}
// BuildExecCommand constructs the full command string for execution in stdio mode
@ -148,15 +145,7 @@ func (c *Config) BuildExecCommand() string {
return ""
}
if runtime.GOOS == "windows" {
// For Windows, we need to properly escape the inner command
escapedCommand := strings.ReplaceAll(c.Stdio.UserCommand, `"`, `\"`)
return fmt.Sprintf(
`npx -y supergateway --stdio "%s" --port %d --baseUrl %s --ssePath %s --messagePath %s`,
escapedCommand, c.Port, c.BaseURL, c.Paths.SSE, c.Paths.Messages,
)
}
// Construct the full command
return fmt.Sprintf(
`npx -y supergateway --stdio "%s" --port %d --baseUrl %s --ssePath %s --messagePath %s`,
c.Stdio.UserCommand, c.Port, c.BaseURL, c.Paths.SSE, c.Paths.Messages,

View file

@ -138,13 +138,18 @@ func TestGetMCPPaths(t *testing.T) {
Paths: PathsConfig{
SSE: "/custom-sse",
Messages: "/custom-messages",
StreamableHTTP: "/custom-streamable",
},
}
paths := cfg.GetMCPPaths()
if len(paths) != 3 {
t.Errorf("Expected 3 MCP paths, got %d", len(paths))
if len(paths) != 2 {
t.Errorf("Expected 2 MCP paths, got %d", len(paths))
}
if paths[0] != "/custom-sse" {
t.Errorf("Expected first path=/custom-sse, got %s", paths[0])
}
if paths[1] != "/custom-messages" {
t.Errorf("Expected second path=/custom-messages, got %s", paths[1])
}
}

View file

@ -10,7 +10,7 @@ import (
"github.com/wso2/open-mcp-auth-proxy/internal/authz"
"github.com/wso2/open-mcp-auth-proxy/internal/config"
logger "github.com/wso2/open-mcp-auth-proxy/internal/logging"
"github.com/wso2/open-mcp-auth-proxy/internal/logging"
"github.com/wso2/open-mcp-auth-proxy/internal/util"
)

View file

@ -4,14 +4,13 @@ import (
"fmt"
"os"
"os/exec"
"runtime"
"strings"
"sync"
"syscall"
"time"
"strings"
"github.com/wso2/open-mcp-auth-proxy/internal/config"
logger "github.com/wso2/open-mcp-auth-proxy/internal/logging"
"github.com/wso2/open-mcp-auth-proxy/internal/logging"
)
// Manager handles starting and graceful shutdown of subprocesses
@ -41,12 +40,7 @@ func EnsureDependenciesAvailable(command string) error {
// Try to install npx using npm
logger.Info("npx not found, attempting to install...")
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
cmd = exec.Command("npm.cmd", "install", "-g", "npx")
} else {
cmd = exec.Command("npm", "install", "-g", "npx")
}
cmd := exec.Command("npm", "install", "-g", "npx")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
@ -94,13 +88,8 @@ func (m *Manager) Start(cfg *config.Config) error {
logger.Info("Starting subprocess with command: %s", execCommand)
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
// Use PowerShell on Windows for better quote handling
cmd = exec.Command("powershell", "-Command", execCommand)
} else {
cmd = exec.Command("sh", "-c", execCommand)
}
// Use the shell to execute the command
cmd := exec.Command("sh", "-c", execCommand)
// Set working directory if specified
if cfg.Stdio.WorkDir != "" {
@ -116,8 +105,8 @@ func (m *Manager) Start(cfg *config.Config) error {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// Set platform-specific process attributes
setProcAttr(cmd)
// Set the process group for proper termination
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
// Start the process
if err := cmd.Start(); err != nil {
@ -128,13 +117,11 @@ func (m *Manager) Start(cfg *config.Config) error {
m.cmd = cmd
logger.Info("Subprocess started with PID: %d", m.process.Pid)
// Get and store the process group ID (Unix) or PID (Windows)
pgid, err := getProcessGroup(m.process.Pid)
// Get and store the process group ID
pgid, err := syscall.Getpgid(m.process.Pid)
if err == nil {
m.processGroup = pgid
if runtime.GOOS != "windows" {
logger.Debug("Process group ID: %d", m.processGroup)
}
} else {
logger.Warn("Failed to get process group ID: %v", err)
m.processGroup = m.process.Pid
@ -182,36 +169,12 @@ func (m *Manager) Shutdown() {
go func() {
defer close(terminateComplete)
// Try graceful termination first
// Try graceful termination first with SIGTERM
terminatedGracefully := false
if runtime.GOOS == "windows" {
// Windows: Try to terminate the process
m.mutex.Lock()
if m.process != nil {
err := m.process.Kill()
if err != nil {
logger.Warn("Failed to terminate process: %v", err)
}
}
m.mutex.Unlock()
// Wait a bit to see if it terminates
for i := 0; i < 10; i++ {
time.Sleep(200 * time.Millisecond)
m.mutex.Lock()
if m.process == nil {
terminatedGracefully = true
m.mutex.Unlock()
break
}
m.mutex.Unlock()
}
} else {
// Unix: Use SIGTERM followed by SIGKILL if necessary
// Try to terminate the process group first
if processGroupToTerminate != 0 {
err := killProcessGroup(processGroupToTerminate, syscall.SIGTERM)
err := syscall.Kill(-processGroupToTerminate, syscall.SIGTERM)
if err != nil {
logger.Warn("Failed to send SIGTERM to process group: %v", err)
@ -249,7 +212,6 @@ func (m *Manager) Shutdown() {
}
m.mutex.Unlock()
}
}
if terminatedGracefully {
logger.Info("Subprocess terminated gracefully")
@ -259,20 +221,9 @@ func (m *Manager) Shutdown() {
// If the process didn't exit gracefully, force kill
logger.Warn("Subprocess didn't exit gracefully, forcing termination...")
if runtime.GOOS == "windows" {
// On Windows, Kill() is already forceful
m.mutex.Lock()
if m.process != nil {
if err := m.process.Kill(); err != nil {
logger.Error("Failed to kill process: %v", err)
}
}
m.mutex.Unlock()
} else {
// Unix: Try SIGKILL
// Try to kill the process group first
if processGroupToTerminate != 0 {
if err := killProcessGroup(processGroupToTerminate, syscall.SIGKILL); err != nil {
if err := syscall.Kill(-processGroupToTerminate, syscall.SIGKILL); err != nil {
logger.Warn("Failed to send SIGKILL to process group: %v", err)
// Fallback to killing just the process
@ -294,7 +245,6 @@ func (m *Manager) Shutdown() {
}
m.mutex.Unlock()
}
}
// Wait a bit more to confirm termination
time.Sleep(500 * time.Millisecond)

View file

@ -1,23 +0,0 @@
//go:build !windows
package subprocess
import (
"os/exec"
"syscall"
)
// setProcAttr sets Unix-specific process attributes
func setProcAttr(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
}
// getProcessGroup gets the process group ID on Unix systems
func getProcessGroup(pid int) (int, error) {
return syscall.Getpgid(pid)
}
// killProcessGroup kills a process group on Unix systems
func killProcessGroup(pgid int, signal syscall.Signal) error {
return syscall.Kill(-pgid, signal)
}

View file

@ -1,27 +0,0 @@
//go:build windows
package subprocess
import (
"os/exec"
"syscall"
)
// setProcAttr sets Windows-specific process attributes
func setProcAttr(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
}
}
// getProcessGroup returns the PID itself on Windows (no process groups)
func getProcessGroup(pid int) (int, error) {
return pid, nil
}
// killProcessGroup kills a process on Windows (no process groups)
func killProcessGroup(pgid int, signal syscall.Signal) error {
// On Windows, we'll use the process handle directly
// This function shouldn't be called on Windows, but we provide it for compatibility
return nil
}