feat(mcp): add list_secrets tool for MCP API
Implements list_secrets MCP tool to query available secrets across global, organization, and repository scopes. Returns secret names, descriptions, and metadata without exposing values. Supports optional owner and repo parameters to filter by scope.
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user