From 61d3c7e7e1093ff50dfa0af239502f9e4a6a7b1e Mon Sep 17 00:00:00 2001 From: Chiran Fernando Date: Sat, 5 Apr 2025 09:37:43 +0530 Subject: [PATCH] Add support for SSE and stdio transport modes --- README.md | 60 ++++++++++++++++++++-- cmd/proxy/main.go | 53 +++++++++---------- config.yaml | 7 ++- internal/config/config.go | 105 +++++++++++++++++++++++++++++++++----- 4 files changed, 177 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index d00f16e..a0cff62 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,44 @@ go build -o openmcpauthproxy ./cmd/proxy ## Using Open MCP Auth Proxy +### Transport Modes + +The Open MCP Auth Proxy supports two transport modes: + +1. **SSE Mode (Default)**: For MCP servers that use Server-Sent Events transport +2. **stdio Mode**: For MCP servers that use stdio transport, which requires starting a MCP Server as a subprocess + +You can specify the transport mode in the `config.yaml` file: + +```yaml +transport_mode: "sse" # Options: "sse" or "stdio" +``` + +Or use the `--stdio` flag to override the configuration: + +```bash +./openmcpauthproxy --stdio +``` + +**Configuration Requirements by Transport Mode:** + +**SSE Mode:** +- `mcp_server_base_url` is required (points to an external MCP server) +- The `command` section is optional and will be ignored +- No subprocess will be started +- The proxy expects an external MCP server to be running at the specified URL + +**stdio Mode:** +- The `command` section in `config.yaml` is mandatory +- `mcp_server_base_url` is optional (if not specified, it will use `command.base_url`) +- The proxy will start a subprocess as specified in the command configuration +- The subprocess will be terminated when the proxy shuts down + ### Quick Start Allows you to just enable authentication and authorization for your MCP server with the preconfigured auth provider powered by Asgardeo. -If you don’t have an MCP server, follow the instructions given here to start your own MCP server for testing purposes. +If you don't have an MCP server, follow the instructions given here to start your own MCP server for testing purposes. 1. Navigate to `resources` directory. 2. Initialize a virtual environment. @@ -60,13 +93,28 @@ python3 echo_server.py Update the following parameters in `config.yaml`. -### demo mode configuration: +### Configuration examples: +**SSE mode (using external MCP server):** ```yaml -mcp_server_base_url: "http://localhost:8000" # URL of your MCP server +transport_mode: "sse" # Transport mode: "sse" or "stdio" +mcp_server_base_url: "http://localhost:8000" # URL of your MCP server (required in SSE mode) listen_port: 8080 # Address where the proxy will listen ``` +**stdio mode (using subprocess):** +```yaml +transport_mode: "stdio" # Transport mode: "sse" or "stdio" + +command: + enabled: true # Must be true in stdio mode + user_command: "npx -y @modelcontextprotocol/server-github" # Required in stdio mode + base_url: "http://localhost:8000" # Used as MCP server base URL if not specified above + port: 8000 + sse_path: "/sse" # SSE endpoint path + message_path: "/messages" # Messages endpoint path +``` + #### Start the Auth Proxy ```bash @@ -86,7 +134,7 @@ Enable authorization for the MCP server through your own Asgardeo organization 1. [Register]([url](https://asgardeo.io/signup)) and create an organization in Asgardeo 2. Now, you need to authorize the OpenMCPAuthProxy to allow dynamically registering MCP Clients as applications in your organization. To do that, 1. Create an [M2M application](https://wso2.com/asgardeo/docs/guides/applications/register-machine-to-machine-app/) - 1. [Authorize this application](https://wso2.com/asgardeo/docs/guides/applications/register-machine-to-machine-app/#authorize-the-api-resources-for-the-app) to invoke “Application Management API” with the `internal_application_mgt_create` scope. + 1. [Authorize this application](https://wso2.com/asgardeo/docs/guides/applications/register-machine-to-machine-app/#authorize-the-api-resources-for-the-app) to invoke "Application Management API" with the `internal_application_mgt_create` scope. ![image](https://github.com/user-attachments/assets/0bd57cac-1904-48cc-b7aa-0530224bc41a) 2. Note the **Client ID** and **Client secret** of this application. This is required by the auth proxy @@ -97,6 +145,7 @@ Create a configuration file config.yaml with the following parameters: ```yaml mcp_server_base_url: "http://localhost:8000" # URL of your MCP server listen_port: 8080 # Address where the proxy will listen +transport_mode: "sse" # Transport mode: "sse" or "stdio" asgardeo: org_name: "" # Your Asgardeo org name @@ -121,6 +170,7 @@ Create a configuration file config.yaml with the following parameters: ```yaml mcp_server_base_url: "http://localhost:8000" # URL of your MCP server listen_port: 8080 # Address where the proxy will listen +transport_mode: "sse" # Transport mode: "sse" or "stdio" ``` **TODO**: Update the configs for a standard OAuth Server. @@ -131,4 +181,4 @@ listen_port: 8080 # Address where the proxy will lis ``` #### Integrating with existing OAuth Providers - - [Auth0](docs/Auth0.md) - Enable authorization for the MCP server through your Auth0 organization. + - [Auth0](docs/Auth0.md) - Enable authorization for the MCP server through your Auth0 organization. \ No newline at end of file diff --git a/cmd/proxy/main.go b/cmd/proxy/main.go index d116fea..6886d9b 100644 --- a/cmd/proxy/main.go +++ b/cmd/proxy/main.go @@ -22,6 +22,7 @@ func main() { demoMode := flag.Bool("demo", false, "Use Asgardeo-based provider (demo).") asgardeoMode := flag.Bool("asgardeo", false, "Use Asgardeo-based provider (asgardeo).") debugMode := flag.Bool("debug", false, "Enable debug logging") + stdioMode := flag.Bool("stdio", false, "Use stdio transport mode instead of SSE") flag.Parse() logger.SetDebug(*debugMode) @@ -33,39 +34,31 @@ func main() { os.Exit(1) } - // 2. Ensure MCPPaths includes the configured paths from the command - if cfg.Command.Enabled { - // Add SSE path to MCPPaths if not already present - ssePath := cfg.Command.SsePath - if ssePath == "" { - ssePath = "/sse" // default + // Override transport mode if stdio flag is set + if *stdioMode { + cfg.TransportMode = config.StdioTransport + // Validate command config for stdio mode + if err := cfg.Command.Validate(cfg.TransportMode); err != nil { + logger.Error("Configuration error: %v", err) + os.Exit(1) } - - messagePath := cfg.Command.MessagePath - if messagePath == "" { - messagePath = "/messages" // default - } - - // Make sure paths are in MCPPaths - ensurePathInList(&cfg.MCPPaths, ssePath) - ensurePathInList(&cfg.MCPPaths, messagePath) - - // Configure baseUrl - baseUrl := cfg.Command.BaseUrl - if baseUrl == "" { - if cfg.Command.Port > 0 { - baseUrl = fmt.Sprintf("http://localhost:%d", cfg.Command.Port) - } else { - baseUrl = "http://localhost:8000" // default - } - } - - logger.Info("Using MCP server baseUrl: %s", baseUrl) } - // 3. Start subprocess if configured + // 2. Ensure MCPPaths are properly configured + if cfg.TransportMode == config.StdioTransport && cfg.Command.Enabled { + // Use command.base_url for MCPServerBaseURL in stdio mode + cfg.MCPServerBaseURL = cfg.Command.GetBaseURL() + + // Use command paths for MCPPaths in stdio mode + cfg.MCPPaths = cfg.Command.GetPaths() + + logger.Info("Using MCP server baseUrl: %s", cfg.MCPServerBaseURL) + logger.Info("Using MCP paths: %v", cfg.MCPPaths) + } + + // 3. Start subprocess if configured and in stdio mode var procManager *subprocess.Manager - if cfg.Command.Enabled && cfg.Command.UserCommand != "" { + if cfg.TransportMode == config.StdioTransport && cfg.Command.Enabled && cfg.Command.UserCommand != "" { // Ensure all required dependencies are available if err := subprocess.EnsureDependenciesAvailable(cfg.Command.UserCommand); err != nil { logger.Warn("%v", err) @@ -76,6 +69,8 @@ func main() { if err := procManager.Start(&cfg.Command); err != nil { logger.Warn("Failed to start subprocess: %v", err) } + } else if cfg.TransportMode == config.SSETransport { + logger.Info("Using SSE transport mode, not starting subprocess") } // 4. Create the chosen provider diff --git a/config.yaml b/config.yaml index 867216c..e7e0537 100644 --- a/config.yaml +++ b/config.yaml @@ -1,10 +1,13 @@ # config.yaml +transport_mode: "stdio" # Options: "sse" or "stdio" + +# For SSE mode, mcp_server_base_url and mcp_paths are required +# For stdio mode, both are optional and will be derived from command configuration if not specified mcp_server_base_url: "http://localhost:8000" listen_port: 8080 timeout_seconds: 10 - -mcp_paths: +mcp_paths: # Required in SSE mode, ignored in stdio mode (derived from command) - /messages/ - /sse diff --git a/internal/config/config.go b/internal/config/config.go index 5e46d6d..c9e4118 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,11 +1,20 @@ package config import ( - "os" "fmt" + "os" + "gopkg.in/yaml.v2" ) +// Transport mode for MCP server +type TransportMode string + +const ( + SSETransport TransportMode = "sse" + StdioTransport TransportMode = "stdio" +) + // AsgardeoConfig groups all Asgardeo-specific fields type DemoConfig struct { ClientID string `yaml:"client_id"` @@ -60,21 +69,22 @@ type DefaultConfig struct { } type Config struct { - AuthServerBaseURL string - MCPServerBaseURL string `yaml:"mcp_server_base_url"` - ListenPort int `yaml:"listen_port"` - JWKSURL string - TimeoutSeconds int `yaml:"timeout_seconds"` - MCPPaths []string `yaml:"mcp_paths"` - PathMapping map[string]string `yaml:"path_mapping"` - Mode string `yaml:"mode"` - CORSConfig CORSConfig `yaml:"cors"` + AuthServerBaseURL string + MCPServerBaseURL string `yaml:"mcp_server_base_url"` + ListenPort int `yaml:"listen_port"` + JWKSURL string + TimeoutSeconds int `yaml:"timeout_seconds"` + MCPPaths []string `yaml:"mcp_paths"` + PathMapping map[string]string `yaml:"path_mapping"` + Mode string `yaml:"mode"` + CORSConfig CORSConfig `yaml:"cors"` + TransportMode TransportMode `yaml:"transport_mode"` // Nested config for Asgardeo Demo DemoConfig `yaml:"demo"` Asgardeo AsgardeoConfig `yaml:"asgardeo"` Default DefaultConfig `yaml:"default"` - Command Command `yaml:"command"` // Command to run + Command Command `yaml:"command"` // Command to run } // Command struct with explicit configuration for all relevant paths @@ -90,6 +100,51 @@ type Command struct { Env []string `yaml:"env,omitempty"` // Environment variables } +// Validate checks if the command config is valid based on transport mode +func (c *Command) Validate(transportMode TransportMode) error { + if transportMode == StdioTransport { + if !c.Enabled { + return fmt.Errorf("command must be enabled in stdio transport mode") + } + if c.UserCommand == "" { + return fmt.Errorf("user_command is required in stdio transport mode") + } + } + return nil +} + +// GetBaseURL returns the base URL for the MCP server +func (c *Command) GetBaseURL() string { + if c.BaseUrl != "" { + return c.BaseUrl + } + if c.Port > 0 { + return fmt.Sprintf("http://localhost:%d", c.Port) + } + return "http://localhost:8000" // default +} + +// GetPaths returns the SSE and message paths +func (c *Command) GetPaths() []string { + var paths []string + + // Add SSE path + ssePath := c.SsePath + if ssePath == "" { + ssePath = "/sse" // default + } + paths = append(paths, ssePath) + + // Add message path + messagePath := c.MessagePath + if messagePath == "" { + messagePath = "/messages" // default + } + paths = append(paths, messagePath) + + return paths +} + // BuildExecCommand constructs the full command string for execution func (c *Command) BuildExecCommand() string { if c.UserCommand == "" { @@ -148,5 +203,31 @@ func LoadConfig(path string) (*Config, error) { if cfg.TimeoutSeconds == 0 { cfg.TimeoutSeconds = 15 // default } + + // Set default transport mode if not specified + if cfg.TransportMode == "" { + cfg.TransportMode = SSETransport // Default to SSE + } + + // Validate command config based on transport mode + if err := cfg.Command.Validate(cfg.TransportMode); err != nil { + return nil, err + } + + // In stdio mode, use command.base_url for MCPServerBaseURL if it's not explicitly set + if cfg.TransportMode == StdioTransport && cfg.MCPServerBaseURL == "" { + cfg.MCPServerBaseURL = cfg.Command.GetBaseURL() + } else if cfg.TransportMode == SSETransport && cfg.MCPServerBaseURL == "" { + return nil, fmt.Errorf("mcp_server_base_url is required in SSE transport mode") + } + + // In stdio mode, set the MCPPaths from the command configuration + if cfg.TransportMode == StdioTransport && cfg.Command.Enabled { + // Override MCPPaths with paths from command configuration + cfg.MCPPaths = cfg.Command.GetPaths() + } else if cfg.TransportMode == SSETransport && len(cfg.MCPPaths) == 0 { + return nil, fmt.Errorf("mcp_paths are required in SSE transport mode") + } + return &cfg, nil -} +} \ No newline at end of file