Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3e270ba
implementing auth, dynamic tool registry, updating mcp-go library
gautambaghel Jul 31, 2025
4c8a775
Update pkg/client/middleware.go
gautambaghel Jul 31, 2025
e82de50
removing unnecessary comment
gautambaghel Jul 31, 2025
39f57ed
Update cmd/terraform-mcp-server/init.go
gautambaghel Jul 31, 2025
9586b7e
Update pkg/client/client.go
gautambaghel Jul 31, 2025
2ac2692
Update pkg/client/client.go
gautambaghel Jul 31, 2025
2d95e85
Update pkg/client/client.go
gautambaghel Jul 31, 2025
ce4ecc0
Update pkg/tools/search_policies.go
gautambaghel Jul 31, 2025
f6df99c
Update pkg/tools/resolve_provider_doc_id.go
gautambaghel Jul 31, 2025
ee6d810
Update pkg/tools/policy_details.go
gautambaghel Jul 31, 2025
144673d
Update pkg/tools/get_provider_docs.go
gautambaghel Jul 31, 2025
b25173f
Update pkg/client/client.go
gautambaghel Jul 31, 2025
a6b3dbb
Update pkg/client/client.go
gautambaghel Jul 31, 2025
d3073d2
Update pkg/resources/resource_templates.go
gautambaghel Jul 31, 2025
cd5150d
Update pkg/tools/dynamic_tool.go
gautambaghel Jul 31, 2025
b375622
Update pkg/tools/dynamic_tool.go
gautambaghel Jul 31, 2025
36eb20d
Update pkg/tools/dynamic_tool.go
gautambaghel Jul 31, 2025
2fd4db5
Merge branch 'main' into feature/auth-tfe
gautambaghel Jul 31, 2025
d3e1040
fixing security comments and copilot issues
gautambaghel Aug 1, 2025
f890f80
Resolve merge conflict by incorporating both suggestions
gautambaghel Aug 7, 2025
1078afd
Merge branch 'main' into feature/auth-tfe
gautambaghel Aug 12, 2025
cd945ed
Merge branch 'main' into feature/auth-tfe
gautambaghel Aug 12, 2025
8352060
updating changelog, go libs and minor fixes
gautambaghel Aug 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
## 0.3.0

* Authentication for HCP Terraform & TFE and restructure the repo. See [#121](https://github.com/hashicorp/terraform-mcp-server/pull/121)
* Implement dynamic tool registration. See [#121](https://github.com/hashicorp/terraform-mcp-server/pull/121)
* Adding 2 new HCP TF/TFE tools. List Terraform organizations & projects. See [#121](https://github.com/hashicorp/terraform-mcp-server/pull/121)
* Changes to tool names to be more consistent. See [#121](https://github.com/hashicorp/terraform-mcp-server/pull/121)
* Implement pagination utility. See [#121](https://github.com/hashicorp/terraform-mcp-server/pull/121)
* Updating `mark3labs/mcp-go` and `hashicorp/tfe-go` versions. See [#121](https://github.com/hashicorp/terraform-mcp-server/pull/121)

## 0.2.2 (Aug 5, 2025)

FEATURES
Expand Down
207 changes: 171 additions & 36 deletions cmd/terraform-mcp-server/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,55 +10,95 @@ import (
stdlog "log"
"net/http"
"os"
"strconv"
"path"
"strings"
"time"

"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-retryablehttp"
"github.com/hashicorp/terraform-mcp-server/pkg/client"
"github.com/hashicorp/terraform-mcp-server/pkg/resources"
"github.com/hashicorp/terraform-mcp-server/pkg/tools"

"github.com/hashicorp/terraform-mcp-server/version"
"github.com/mark3labs/mcp-go/server"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

func InitRegistryClient(logger *log.Logger) *http.Client {
retryClient := retryablehttp.NewClient()
retryClient.Logger = logger
var (
rootCmd = &cobra.Command{
Use: "terraform-mcp-server",
Short: "Terraform MCP Server",
Long: `A Terraform MCP server that handles various tools and resources.`,
Version: fmt.Sprintf("Version: %s\nCommit: %s\nBuild Date: %s", version.GetHumanVersion(), version.GitCommit, version.BuildDate),
Run: runDefaultCommand,
}

transport := cleanhttp.DefaultPooledTransport()
transport.Proxy = http.ProxyFromEnvironment
stdioCmd = &cobra.Command{
Use: "stdio",
Short: "Start stdio server",
Long: `Start a server that communicates via standard input/output streams using JSON-RPC messages.`,
Run: func(_ *cobra.Command, _ []string) {
logFile, err := rootCmd.PersistentFlags().GetString("log-file")
if err != nil {
stdlog.Fatal("Failed to get log file:", err)
}
logger, err := initLogger(logFile)
if err != nil {
stdlog.Fatal("Failed to initialize logger:", err)
}

retryClient.HTTPClient = cleanhttp.DefaultClient()
retryClient.HTTPClient.Timeout = 10 * time.Second
retryClient.HTTPClient.Transport = transport
retryClient.RetryMax = 3
if err := runStdioServer(logger); err != nil {
stdlog.Fatal("failed to run stdio server:", err)
}
},
}

retryClient.Backoff = func(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
if resp != nil && resp.StatusCode == http.StatusTooManyRequests {
resetAfter := resp.Header.Get("x-ratelimit-reset")
resetAfterInt, err := strconv.ParseInt(resetAfter, 10, 64)
streamableHTTPCmd = &cobra.Command{
Use: "streamable-http",
Short: "Start StreamableHTTP server",
Long: `Start a server that communicates via StreamableHTTP transport on port 8080 at /mcp endpoint.`,
Run: func(cmd *cobra.Command, _ []string) {
logFile, err := rootCmd.PersistentFlags().GetString("log-file")
if err != nil {
return 0
stdlog.Fatal("Failed to get log file:", err)
}
logger, err := initLogger(logFile)
if err != nil {
stdlog.Fatal("Failed to initialize logger:", err)
}
resetAfterTime := time.Unix(resetAfterInt, 0)
return time.Until(resetAfterTime)
}
return 0
}

retryClient.CheckRetry = func(ctx context.Context, resp *http.Response, err error) (bool, error) {
if resp != nil && resp.StatusCode == http.StatusTooManyRequests {
resetAfter := resp.Header.Get("x-ratelimit-reset")
return resetAfter != "", nil
}
return false, nil
port, err := cmd.Flags().GetString("transport-port")
if err != nil {
stdlog.Fatal("Failed to get streamableHTTP port:", err)
}
host, err := cmd.Flags().GetString("transport-host")
if err != nil {
stdlog.Fatal("Failed to get streamableHTTP host:", err)
}

endpointPath, err := cmd.Flags().GetString("mcp-endpoint")
if err != nil {
stdlog.Fatal("Failed to get endpoint path:", err)
}

if err := runHTTPServer(logger, host, port, endpointPath); err != nil {
stdlog.Fatal("failed to run streamableHTTP server:", err)
}
},
}

return retryClient.StandardClient()
}
// Create an alias for backward compatibility
httpCmdAlias = &cobra.Command{
Use: "http",
Short: "Start StreamableHTTP server (deprecated, use 'streamable-http' instead)",
Long: `This command is deprecated. Please use 'streamable-http' instead.`,
Deprecated: "Use 'streamable-http' instead",
Run: func(cmd *cobra.Command, args []string) {
// Forward to the new command
streamableHTTPCmd.Run(cmd, args)
},
}
)

func init() {
cobra.OnInitialize(initConfig)
Expand Down Expand Up @@ -101,11 +141,11 @@ func initLogger(outPath string) (*log.Logger, error) {
return logger, nil
}

func registryInit(hcServer *server.MCPServer, logger *log.Logger) {
registryClient := InitRegistryClient(logger)
tools.InitTools(hcServer, registryClient, logger)
resources.RegisterResources(hcServer, registryClient, logger)
resources.RegisterResourceTemplates(hcServer, registryClient, logger)
// registerToolsAndResources registers tools and resources with the MCP server
func registerToolsAndResources(hcServer *server.MCPServer, logger *log.Logger) {
tools.RegisterTools(hcServer, logger)
resources.RegisterResources(hcServer, logger)
resources.RegisterResourceTemplates(hcServer, logger)
}

func serverInit(ctx context.Context, hcServer *server.MCPServer, logger *log.Logger) error {
Expand Down Expand Up @@ -134,3 +174,98 @@ func serverInit(ctx context.Context, hcServer *server.MCPServer, logger *log.Log

return nil
}

func streamableHTTPServerInit(ctx context.Context, hcServer *server.MCPServer, logger *log.Logger, host string, port string, endpointPath string) error {
// Check if stateless mode is enabled
isStateless := shouldUseStatelessMode()

// Ensure endpoint path starts with /
endpointPath = path.Join("/", endpointPath)
// Create StreamableHTTP server which implements the new streamable-http transport
// This is the modern MCP transport that supports both direct HTTP responses and SSE streams
opts := []server.StreamableHTTPOption{
server.WithEndpointPath(endpointPath), // Default MCP endpoint path
server.WithLogger(logger),
}

// Log the endpoint path being used
logger.Infof("Using endpoint path: %s", endpointPath)

// Only add the WithStateLess option if stateless mode is enabled
// TODO: fix this in mcp-go ver 0.33.0 or higher
if isStateless {
opts = append(opts, server.WithStateLess(true))
logger.Infof("Running in stateless mode")
} else {
logger.Infof("Running in stateful mode (default)")
}

baseStreamableServer := server.NewStreamableHTTPServer(hcServer, opts...)

// Load CORS configuration
corsConfig := client.LoadCORSConfigFromEnv()

// Log CORS configuration
logger.Infof("CORS Mode: %s", corsConfig.Mode)
if len(corsConfig.AllowedOrigins) > 0 {
logger.Infof("Allowed Origins: %s", strings.Join(corsConfig.AllowedOrigins, ", "))
} else if corsConfig.Mode == "strict" {
logger.Warnf("No allowed origins configured in strict mode. All cross-origin requests will be rejected.")
} else if corsConfig.Mode == "development" {
logger.Infof("Development mode: localhost origins are automatically allowed")
} else if corsConfig.Mode == "disabled" {
logger.Warnf("CORS validation is disabled. This is not recommended for production.")
}

// Create a security wrapper around the streamable server
streamableServer := client.NewSecurityHandler(baseStreamableServer, corsConfig.AllowedOrigins, corsConfig.Mode, logger)

mux := http.NewServeMux()

// Apply middleware
streamableServer = client.TerraformContextMiddleware(logger)(streamableServer)

// Handle the /mcp endpoint with the streamable server (with security wrapper)
mux.Handle(endpointPath, streamableServer)
mux.Handle(endpointPath+"/", streamableServer)

// Add health check endpoint
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
response := fmt.Sprintf(`{"status":"ok","service":"terraform-mcp-server","transport":"streamable-http","endpoint":"%s"}`, endpointPath)
w.Write([]byte(response))
})

addr := fmt.Sprintf("%s:%s", host, port)
httpServer := &http.Server{
Addr: addr,
Handler: mux,
ReadTimeout: 30 * time.Second,
ReadHeaderTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}

// Start server in goroutine
errC := make(chan error, 1)
go func() {
logger.Infof("Starting StreamableHTTP server on %s%s", addr, endpointPath)
errC <- httpServer.ListenAndServe()
}()

// Wait for shutdown signal
select {
case <-ctx.Done():
logger.Infof("Shutting down StreamableHTTP server...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return httpServer.Shutdown(shutdownCtx)
case err := <-errC:
if err != nil && err != http.ErrServerClosed {
return fmt.Errorf("StreamableHTTP server error: %w", err)
}
}

return nil
}
Loading
Loading