Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 7 additions & 7 deletions pkg/tools/registry/get_latest_module_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,37 +45,37 @@ func GetLatestModuleVersion(logger *log.Logger) server.ServerTool {
func getLatestModuleVersionHandler(ctx context.Context, request mcp.CallToolRequest, logger *log.Logger) (*mcp.CallToolResult, error) {
modulePublisher, err := request.RequireString("module_publisher")
if err != nil {
return nil, utils.LogAndReturnError(logger, "required input: 'module_publisher' (the publisher of the module)", err)
return utils.ToolError(logger, "required input: 'module_publisher' (the publisher of the module)", err)
}
modulePublisher = strings.ToLower(modulePublisher)

moduleName, err := request.RequireString("module_name")
if err != nil {
return nil, utils.LogAndReturnError(logger, "required input: 'module_name' (the name of the module)", err)
return utils.ToolError(logger, "required input: 'module_name' (the name of the module)", err)
}
moduleName = strings.ToLower(moduleName)

moduleProvider, err := request.RequireString("module_provider")
if err != nil {
return nil, utils.LogAndReturnError(logger, "required input: 'module_provider' (the provider of the module)", err)
return utils.ToolError(logger, "required input: 'module_provider' (the provider of the module)", err)
}
moduleProvider = strings.ToLower(moduleProvider)

// Get a simple http client to access the public Terraform registry from context
httpClient, err := client.GetHttpClientFromContext(ctx, logger)
if err != nil {
logger.WithError(err).Error("failed to get http client for public Terraform registry")
return mcp.NewToolResultError(fmt.Sprintf("failed to get http client for public Terraform registry: %v", err)), nil
return utils.ToolError(logger, "failed to get http client for public Terraform registry", err)
}

uri := fmt.Sprintf("modules/%s/%s/%s", modulePublisher, moduleName, moduleProvider)
response, err := client.SendRegistryCall(httpClient, http.MethodGet, uri, logger)
if err != nil {
return nil, utils.LogAndReturnError(logger, fmt.Sprintf("fetching module information for %s/%s from the %s provider", modulePublisher, moduleName, moduleProvider), err)
return utils.ToolErrorf(logger, "fetching module information for %s/%s from the %s provider: %v", modulePublisher, moduleName, moduleProvider, err)
}

var moduleVersionDetails client.TerraformModuleVersionDetails
if err := json.Unmarshal(response, &moduleVersionDetails); err != nil {
return nil, utils.LogAndReturnError(logger, fmt.Sprintf("unmarshalling module information for %s/%s from the %s provider", modulePublisher, moduleName, moduleProvider), err)
return utils.ToolErrorf(logger, "unmarshalling module information for %s/%s from the %s provider: %v", modulePublisher, moduleName, moduleProvider, err)
}

return mcp.NewToolResultText(moduleVersionDetails.Version), nil
Expand Down
11 changes: 4 additions & 7 deletions pkg/tools/registry/get_latest_provider_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package tools

import (
"context"
"fmt"
"strings"

"github.com/hashicorp/terraform-mcp-server/pkg/client"
Expand Down Expand Up @@ -40,26 +39,24 @@ func GetLatestProviderVersion(logger *log.Logger) server.ServerTool {
func getLatestProviderVersionHandler(ctx context.Context, request mcp.CallToolRequest, logger *log.Logger) (*mcp.CallToolResult, error) {
namespace, err := request.RequireString("namespace")
if err != nil {
return nil, utils.LogAndReturnError(logger, "required input: namespace of the Terraform provider is required", err)
return utils.ToolError(logger, "missing required input: namespace", err)
}
namespace = strings.ToLower(namespace)

name, err := request.RequireString("name")
if err != nil {
return nil, utils.LogAndReturnError(logger, "required input: name of the Terraform provider is required", err)
return utils.ToolError(logger, "missing required input: name", err)
}
name = strings.ToLower(name)

// Get a simple http client to access the public Terraform registry from context
httpClient, err := client.GetHttpClientFromContext(ctx, logger)
if err != nil {
logger.WithError(err).Error("failed to get http client for public Terraform registry")
return mcp.NewToolResultError(fmt.Sprintf("failed to get http client for public Terraform registry: %v", err)), nil
return utils.ToolError(logger, "failed to get http client for public Terraform registry", err)
}

version, err := client.GetLatestProviderVersion(httpClient, namespace, name, logger)
if err != nil {
return nil, utils.LogAndReturnError(logger, "fetching latest provider version", err)
return utils.ToolErrorf(logger, "provider not found: %s/%s - verify the namespace and provider name are correct", namespace, name)
}

return mcp.NewToolResultText(version), nil
Expand Down
37 changes: 14 additions & 23 deletions pkg/tools/registry/get_module_details.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,40 +42,37 @@ func ModuleDetails(logger *log.Logger) server.ServerTool {
func getModuleDetailsHandler(ctx context.Context, request mcp.CallToolRequest, logger *log.Logger) (*mcp.CallToolResult, error) {
moduleID, err := request.RequireString("module_id")
if err != nil {
return nil, utils.LogAndReturnError(logger, "required input: module_id is required", err)
return utils.ToolError(logger, "missing required input: module_id", err)
}
if moduleID == "" {
return nil, utils.LogAndReturnError(logger, "required input: module_id cannot be empty", nil)
return utils.ToolError(logger, "module_id cannot be empty", nil)
}

// Validate module ID format
if err := validateModuleID(moduleID); err != nil {
return nil, utils.LogAndReturnError(logger, err.Error(), nil)
return utils.ToolError(logger, err.Error(), nil)
}

moduleID = strings.ToLower(moduleID)

// Get a simple http client to access the public Terraform registry from context
httpClient, err := client.GetHttpClientFromContext(ctx, logger)
if err != nil {
logger.WithError(err).Error("failed to get http client for public Terraform registry")
return mcp.NewToolResultError(fmt.Sprintf("failed to get http client for public Terraform registry: %v", err)), nil
return utils.ToolError(logger, "failed to get http client for public Terraform registry", err)
}

var errMsg string
response, err := getModuleDetails(httpClient, moduleID, 0, logger)
if err != nil {
errMsg = fmt.Sprintf("getting module(s), none found! module_id: %v,", moduleID)
return nil, utils.LogAndReturnError(logger, errMsg, nil)
return utils.ToolErrorf(logger, "module not found: %s - use search_modules first to find valid module IDs", moduleID)
}

moduleData, err := unmarshalTerraformModule(response)
if err != nil {
return nil, utils.LogAndReturnError(logger, "unmarshalling module details", err)
return utils.ToolError(logger, "failed to parse module details", err)
}
if moduleData == "" {
errMsg = fmt.Sprintf("getting module(s), none found! %s please provider a different moduleProvider", errMsg)
return nil, utils.LogAndReturnError(logger, errMsg, nil)
return utils.ToolErrorf(logger, "no module data returned for %s - try a different module_id", moduleID)
}

return mcp.NewToolResultText(moduleData), nil
}

Expand All @@ -88,20 +85,17 @@ func getModuleDetails(httpClient *http.Client, moduleID string, currentOffset in
uri = fmt.Sprintf("%s?offset=%v", uri, currentOffset)
response, err := client.SendRegistryCall(httpClient, "GET", uri, logger)
if err != nil {
// We shouldn't log the error here because we might hit a namespace that doesn't exist, it's better to let the caller handle it.
return nil, fmt.Errorf("getting module(s) for: %v, please provide a different provider name like aws, azurerm or google etc", moduleID)
}

// Return the filtered JSON as a string
return response, nil
}

func unmarshalTerraformModule(response []byte) (string, error) {
// Handles one module
var terraformModules client.TerraformModuleVersionDetails
err := json.Unmarshal(response, &terraformModules)
if err != nil {
return "", utils.LogAndReturnError(nil, "unmarshalling module details", err)
return "", fmt.Errorf("unmarshalling module details: %w", err)
}

var builder strings.Builder
Expand All @@ -120,7 +114,7 @@ func unmarshalTerraformModule(response []byte) (string, error) {
builder.WriteString(fmt.Sprintf("| %s | %s | %s | `%v` | %t |\n",
input.Name,
input.Type,
input.Description, // Consider cleaning potential newlines/markdown
input.Description,
input.Default,
input.Required,
))
Expand All @@ -136,7 +130,7 @@ func unmarshalTerraformModule(response []byte) (string, error) {
for _, output := range terraformModules.Root.Outputs {
builder.WriteString(fmt.Sprintf("| %s | %s |\n",
output.Name,
output.Description, // Consider cleaning potential newlines/markdown
output.Description,
))
}
builder.WriteString("\n")
Expand All @@ -163,11 +157,8 @@ func unmarshalTerraformModule(response []byte) (string, error) {
builder.WriteString("### Examples\n\n")
for _, example := range terraformModules.Examples {
builder.WriteString(fmt.Sprintf("#### %s\n\n", example.Name))
// Optionally, include more details from example if needed, like inputs/outputs
// For now, just listing the name.
if example.Readme != "" {
builder.WriteString("**Readme:**\n\n")
// Append readme content, potentially needs markdown escaping/sanitization depending on source
builder.WriteString(example.Readme)
builder.WriteString("\n\n")
}
Expand Down
15 changes: 6 additions & 9 deletions pkg/tools/registry/get_policy_details.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,26 +41,25 @@ func PolicyDetails(logger *log.Logger) server.ServerTool {
func getPolicyDetailsHandler(ctx context.Context, request mcp.CallToolRequest, logger *log.Logger) (*mcp.CallToolResult, error) {
terraformPolicyID, err := request.RequireString("terraform_policy_id")
if err != nil {
return nil, utils.LogAndReturnError(logger, "required input: terraform_policy_id is required and must be a string, it is fetched by running the search_policies tool", err)
return utils.ToolError(logger, "missing required input: terraform_policy_id - use search_policies first to find valid policy IDs", err)
}
if terraformPolicyID == "" {
return nil, utils.LogAndReturnError(logger, "required input: terraform_policy_id cannot be empty, it is fetched by running the search_policies tool", nil)
return utils.ToolError(logger, "terraform_policy_id cannot be empty - use search_policies first to find valid policy IDs", nil)
}

// Get a simple http client to access the public Terraform registry from context
httpClient, err := client.GetHttpClientFromContext(ctx, logger)
if err != nil {
logger.WithError(err).Error("failed to get http client for public Terraform registry")
return mcp.NewToolResultError(fmt.Sprintf("failed to get http client for public Terraform registry: %v", err)), nil
return utils.ToolError(logger, "failed to get http client for public Terraform registry", err)
}

policyResp, err := client.SendRegistryCall(httpClient, "GET", (&url.URL{Path: terraformPolicyID, RawQuery: url.Values{"include": {"policies,policy-modules,policy-library"}}.Encode()}).String(), logger, "v2")
if err != nil {
return nil, utils.LogAndReturnError(logger, "fetching policy details: registry API did not return a successful response", err)
return utils.ToolErrorf(logger, "policy not found: %s - verify the terraform_policy_id is correct or use search_policies to find valid IDs", terraformPolicyID)
}

var policyDetails client.TerraformPolicyDetails
if err := json.Unmarshal(policyResp, &policyDetails); err != nil {
return nil, utils.LogAndReturnError(logger, fmt.Sprintf("unmarshalling policy details for %s", terraformPolicyID), err)
return utils.ToolErrorf(logger, "failed to parse policy details for %s", terraformPolicyID)
}

readme := utils.ExtractReadme(policyDetails.Data.Attributes.Readme)
Expand All @@ -70,7 +69,6 @@ func getPolicyDetailsHandler(ctx context.Context, request mcp.CallToolRequest, l
moduleList := ""
for _, policy := range policyDetails.Included {
if policy.Type == "policy-modules" {
// Use text/template to safely build the module block
var moduleBuilder strings.Builder
tmpl := `
module "{{.Name}}" {
Expand Down Expand Up @@ -103,7 +101,6 @@ module "{{.Name}}" {
builder.WriteString("## Usage\n\n")
builder.WriteString("Generate the content for a HashiCorp Configuration Language (HCL) file named policies.hcl. This file should define a set of policies. For each policy provided, create a distinct policy block using the following template.\n")
builder.WriteString("\n```hcl\n")
// Use text/template to safely build the HCL template for policies
hclTmpl := `
{{- if .ModuleList }}
{{ .ModuleList }}
Expand Down
23 changes: 7 additions & 16 deletions pkg/tools/registry/get_provider_capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,60 +57,53 @@ Returns a summary with counts and examples for each capability type.`),
func getProviderCapabilitiesHandler(ctx context.Context, request mcp.CallToolRequest, logger *log.Logger) (*mcp.CallToolResult, error) {
namespace, err := request.RequireString("namespace")
if err != nil {
return nil, utils.LogAndReturnError(logger, "required input: namespace of the Terraform provider is required", err)
return utils.ToolError(logger, "missing required input: namespace", err)
}
namespace = strings.ToLower(namespace)

name, err := request.RequireString("name")
if err != nil {
return nil, utils.LogAndReturnError(logger, "required input: name of the Terraform provider is required", err)
return utils.ToolError(logger, "missing required input: name", err)
}
name = strings.ToLower(name)

version := request.GetString("version", "latest")
if version == "latest" || !utils.IsValidProviderVersionFormat(version) {
// Get a simple http client to access the public Terraform registry from context
httpClient, err := client.GetHttpClientFromContext(ctx, logger)
if err != nil {
logger.WithError(err).Error("failed to get http client for public Terraform registry")
return mcp.NewToolResultError(fmt.Sprintf("failed to get http client for public Terraform registry: %v", err)), nil
return utils.ToolError(logger, "failed to get http client for public Terraform registry", err)
}

latestVersion, err := client.GetLatestProviderVersion(httpClient, namespace, name, logger)
if err != nil {
return nil, utils.LogAndReturnError(logger, "fetching latest provider version", err)
return utils.ToolErrorf(logger, "provider not found: %s/%s - verify the namespace and provider name are correct", namespace, name)
}
version = latestVersion
}

// Get a simple http client to access the public Terraform registry from context
httpClient, err := client.GetHttpClientFromContext(ctx, logger)
if err != nil {
logger.WithError(err).Error("failed to get http client for public Terraform registry")
return mcp.NewToolResultError(fmt.Sprintf("failed to get http client for public Terraform registry: %v", err)), nil
return utils.ToolError(logger, "failed to get http client for public Terraform registry", err)
}

// Get provider documentation
uri := fmt.Sprintf("providers/%s/%s/%s", namespace, name, version)
response, err := client.SendRegistryCall(httpClient, "GET", uri, logger)
if err != nil {
return nil, utils.LogAndReturnError(logger, fmt.Sprintf("fetching provider docs for %s/%s:%s", namespace, name, version), err)
return utils.ToolErrorf(logger, "failed to fetch provider docs for %s/%s:%s - verify the provider exists", namespace, name, version)
}

var providerDocs client.ProviderDocs
if err := json.Unmarshal(response, &providerDocs); err != nil {
return nil, utils.LogAndReturnError(logger, "unmarshalling provider docs", err)
return utils.ToolErrorf(logger, "failed to parse provider docs for %s/%s:%s", namespace, name, version)
}

// Analyze and format capabilities
output := analyzeAndFormatCapabilities(providerDocs, namespace, name, version)
return mcp.NewToolResultText(output), nil
}

func analyzeAndFormatCapabilities(docs client.ProviderDocs, namespace, name, version string) string {
capabilities := make(map[string][]client.ProviderDoc)

// Analyze documentation
for _, doc := range docs.Docs {
if doc.Language != "hcl" {
continue
Expand All @@ -128,13 +121,11 @@ func analyzeAndFormatCapabilities(docs client.ProviderDocs, namespace, name, ver
return builder.String()
}

// Show all capabilities as discovered
for capType, items := range capabilities {
title := strings.ReplaceAll(capType, "-", " ")
title = cases.Title(language.English).String(title)
builder.WriteString(fmt.Sprintf("%s: %d available\n", title, len(items)))

// Dynamic listing: show all if ≤10, otherwise show 3 with "more" message
limit := 3
if len(items) <= 10 {
limit = len(items)
Expand Down
16 changes: 7 additions & 9 deletions pkg/tools/registry/get_provider_details.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package tools
import (
"context"
"encoding/json"
"fmt"
"path"
"strconv"

Expand Down Expand Up @@ -40,30 +39,29 @@ You must call 'search_providers' tool first to obtain the exact tfprovider-compa
func getProviderDocsHandler(ctx context.Context, request mcp.CallToolRequest, logger *log.Logger) (*mcp.CallToolResult, error) {
providerDocID, err := request.RequireString("provider_doc_id")
if err != nil {
return nil, utils.LogAndReturnError(logger, "required input: provider_doc_id is required", err)
return utils.ToolError(logger, "missing required input: provider_doc_id", err)
}
if providerDocID == "" {
return nil, utils.LogAndReturnError(logger, "required input: provider_doc_id cannot be empty", nil)
return utils.ToolError(logger, "provider_doc_id cannot be empty", nil)
}
if _, err := strconv.Atoi(providerDocID); err != nil {
return nil, utils.LogAndReturnError(logger, "required input: provider_doc_id must be a valid number", err)
return utils.ToolError(logger, "provider_doc_id must be a valid number - use search_providers first to find valid IDs", err)
}

// Get a simple http client to access the public Terraform registry from context
httpClient, err := client.GetHttpClientFromContext(ctx, logger)
if err != nil {
logger.WithError(err).Error("failed to get http client for public Terraform registry")
return mcp.NewToolResultError(fmt.Sprintf("failed to get http client for public Terraform registry: %v", err)), nil
return utils.ToolError(logger, "failed to get http client for public Terraform registry", err)
}

detailResp, err := client.SendRegistryCall(httpClient, "GET", path.Join("provider-docs", providerDocID), logger, "v2")
if err != nil {
return nil, utils.LogAndReturnError(logger, fmt.Sprintf("fetching provider-docs/%s, please make sure provider_doc_id is valid and the search_providers tool has run prior", providerDocID), err)
return utils.ToolErrorf(logger, "provider doc not found: %s - use search_providers first to find valid provider_doc_id values", providerDocID)
}

var details client.ProviderResourceDetails
if err := json.Unmarshal(detailResp, &details); err != nil {
return nil, utils.LogAndReturnError(logger, fmt.Sprintf("unmarshalling provider-docs/%s", providerDocID), err)
return utils.ToolErrorf(logger, "failed to parse provider docs for %s", providerDocID)
}

return mcp.NewToolResultText(details.Data.Attributes.Content), nil
}
Loading
Loading