diff --git a/routers/api/v2/mcp.go b/routers/api/v2/mcp.go index d916e99b3f..9e0042f731 100644 --- a/routers/api/v2/mcp.go +++ b/routers/api/v2/mcp.go @@ -14,6 +14,8 @@ import ( actions_model "code.gitcaddy.com/server/v3/models/actions" "code.gitcaddy.com/server/v3/models/db" repo_model "code.gitcaddy.com/server/v3/models/repo" + secret_model "code.gitcaddy.com/server/v3/models/secret" + user_model "code.gitcaddy.com/server/v3/models/user" "code.gitcaddy.com/server/v3/modules/actions" "code.gitcaddy.com/server/v3/modules/json" "code.gitcaddy.com/server/v3/modules/log" @@ -235,6 +237,23 @@ var mcpTools = []MCPTool{ "required": []string{"owner", "repo", "tag"}, }, }, + { + Name: "list_secrets", + Description: "List available secrets (names and descriptions only, not values) for workflows. Shows global, organization, and repository secrets.", + InputSchema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "owner": map[string]any{ + "type": "string", + "description": "Repository or organization owner (optional, shows global secrets if omitted)", + }, + "repo": map[string]any{ + "type": "string", + "description": "Repository name (optional, shows org secrets if omitted)", + }, + }, + }, + }, } // MCPHandler handles MCP protocol requests @@ -326,6 +345,8 @@ func handleToolsCall(ctx *context.APIContext, req *MCPRequest) { result, err = toolListReleases(ctx, params.Arguments) case "get_release": result, err = toolGetRelease(ctx, params.Arguments) + case "list_secrets": + result, err = toolListSecrets(ctx, params.Arguments) case "get_error_patterns": result, err = toolGetErrorPatterns(ctx, params.Arguments) case "report_error_solution": @@ -789,3 +810,94 @@ func toolGetRelease(ctx *context.APIContext, args map[string]any) (any, error) { "asset_count": len(assets), }, nil } + +func toolListSecrets(ctx *context.APIContext, args map[string]any) (any, error) { + owner, _ := args["owner"].(string) + repo, _ := args["repo"].(string) + + result := map[string]any{ + "global_secrets": []map[string]any{}, + "owner_secrets": []map[string]any{}, + "repo_secrets": []map[string]any{}, + } + + // Always include global secrets (available to all workflows) + globalSecrets, err := db.Find[secret_model.Secret](ctx, secret_model.FindSecretsOptions{Global: true}) + if err != nil { + log.Error("Failed to fetch global secrets: %v", err) + } else { + globalList := make([]map[string]any, 0, len(globalSecrets)) + for _, s := range globalSecrets { + globalList = append(globalList, map[string]any{ + "name": s.Name, + "description": s.Description, + "created_at": s.CreatedUnix.AsTime().Format(time.RFC3339), + "scope": "global", + }) + } + result["global_secrets"] = globalList + } + + // If owner is specified, get org/user secrets + if owner != "" { + ownerUser, err := user_model.GetUserByName(ctx, owner) + if err != nil { + return nil, fmt.Errorf("owner not found: %s", owner) + } + + ownerSecrets, err := db.Find[secret_model.Secret](ctx, secret_model.FindSecretsOptions{OwnerID: ownerUser.ID}) + if err != nil { + log.Error("Failed to fetch owner secrets: %v", err) + } else { + ownerList := make([]map[string]any, 0, len(ownerSecrets)) + for _, s := range ownerSecrets { + scope := "user" + if ownerUser.IsOrganization() { + scope = "organization" + } + ownerList = append(ownerList, map[string]any{ + "name": s.Name, + "description": s.Description, + "created_at": s.CreatedUnix.AsTime().Format(time.RFC3339), + "scope": scope, + "owner": owner, + }) + } + result["owner_secrets"] = ownerList + } + + // If repo is also specified, get repo secrets + if repo != "" { + repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repo) + if err != nil { + return nil, fmt.Errorf("repository not found: %s/%s", owner, repo) + } + + repoSecrets, err := db.Find[secret_model.Secret](ctx, secret_model.FindSecretsOptions{RepoID: repository.ID}) + if err != nil { + log.Error("Failed to fetch repo secrets: %v", err) + } else { + repoList := make([]map[string]any, 0, len(repoSecrets)) + for _, s := range repoSecrets { + repoList = append(repoList, map[string]any{ + "name": s.Name, + "description": s.Description, + "created_at": s.CreatedUnix.AsTime().Format(time.RFC3339), + "scope": "repository", + "repo": fmt.Sprintf("%s/%s", owner, repo), + }) + } + result["repo_secrets"] = repoList + } + } + } + + // Add summary counts + result["total_count"] = len(result["global_secrets"].([]map[string]any)) + + len(result["owner_secrets"].([]map[string]any)) + + len(result["repo_secrets"].([]map[string]any)) + + result["note"] = "Secret values are not shown for security. Only names and descriptions are available." + + return result, nil +}