From 5036babe3be882aa39fe70be139440895de87deb Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Fri, 4 Jul 2025 13:00:38 +0530 Subject: [PATCH 01/11] MCP Server Audit Tool --- client/audit.go | 51 +++++++++++++++++++++++++ client/dto/audit.go | 81 +++++++++++++++++++++++++++++++++++++++ pkg/harness/audit.go | 91 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 223 insertions(+) create mode 100644 client/audit.go create mode 100644 client/dto/audit.go create mode 100644 pkg/harness/audit.go diff --git a/client/audit.go b/client/audit.go new file mode 100644 index 00000000..3bfae193 --- /dev/null +++ b/client/audit.go @@ -0,0 +1,51 @@ +package client + +import ( + "fmt" + "context" + "github.com/harness/harness-mcp/client/dto" +) + +type AuditService struct { + Client *Client +} + +// ListPipelineExecutionsByUser fetches pipeline executions triggered by a specific user. +func (a *AuditService) ListPipelineExecutionsByUser(ctx context.Context, scope dto.Scope, userID string, modules []string, page int, size int, startTime int64, endTime int64, opts *dto.ListAuditEventsFilter) (*dto.AuditOutput[dto.AuditListItem], error) { + if opts == nil { + opts = &dto.ListAuditEventsFilter{} + } + + params := make(map[string]string) + params["routingId"] = scope.AccountID // required for Harness gateway APIs + params["accountIdentifier"] = scope.AccountID + params["pageIndex"] = fmt.Sprintf("%d", page) + params["pageSize"] = fmt.Sprintf("%d", size) + + addScope(scope, params) + + // Required fields + opts.FilterType = "Audit" + opts.Principals = []dto.AuditPrincipal{{ + Type: "USER", + Identifier: userID, + }} + + opts.Scopes = []dto.AuditResourceScope{{ + AccountIdentifier: scope.AccountID, + OrgIdentifier: scope.OrgID, + ProjectIdentifier: scope.ProjectID, + }} + + opts.Modules = modules + opts.StartTime = startTime // or use a date range with UnixMillis + opts.EndTime = endTime + + resp := &dto.AuditOutput[dto.AuditListItem]{} + err := a.Client.Post(ctx, "gateway/audit/api/audits/list", params, opts, resp) + if err != nil { + return nil, fmt.Errorf("failed to list pipeline executions by user: %w", err) + } + + return resp, nil +} \ No newline at end of file diff --git a/client/dto/audit.go b/client/dto/audit.go new file mode 100644 index 00000000..4fc7d413 --- /dev/null +++ b/client/dto/audit.go @@ -0,0 +1,81 @@ +package dto + +type AuditPrincipal struct { + Type string `json:"type"` + Identifier string `json:"identifier"` +} + +type AuditPaginationOptions struct { + Page int `json:"page,omitempty"` + Size int `json:"size,omitempty"` +} + +// AuditListOptions represents the options for listing pipelines +type AuditListOptions struct { + AuditPaginationOptions + SearchTerm string `json:"searchTerm,omitempty"` +} + +type AuditResourceScope struct { + AccountIdentifier string `json:"accountIdentifier"` + OrgIdentifier string `json:"orgIdentifier,omitempty"` + ProjectIdentifier string `json:"projectIdentifier,omitempty"` +} + +type ListAuditEventsFilter struct { + Scopes []AuditResourceScope `json:"scopes,omitempty"` + Principals []AuditPrincipal `json:"principals,omitempty"` + Actions []string `json:"actions,omitempty"` + FilterType string `json:"filterType,omitempty"` + StartTime int64 `json:"startTime,omitempty"` + EndTime int64 `json:"endTime,omitempty"` + Modules []string `json:"modules,omitempty"` +} + + +type AuditOutput[T any] struct { + Status string `json:"status,omitempty"` + Data AuditOutputData[T] `json:"data,omitempty"` +} + +type AuditOutputData[T any] struct { + PageItemCount int `json:"pageItemCount,omitempty"` + PageSize int `json:"pageSize,omitempty"` + Content []T `json:"content,omitempty"` + PageIndex int `json:"pageIndex,omitempty"` + HasNext bool `json:"hasNext,omitempty"` + PageToken string `json:"pageToken,omitempty"` + TotalItems int `json:totalItems, omitempty` + TotalPages int `json:totalPages, omitempty` +} + +type AuditListItem struct { + AuditID string `json:"auditId,omitempty"` + InsertId string `json:"insertId,omitempty"` + Resource AuditResource `json:"resource,omitempty"` + Action string `json:"action,omitempty"` + Module string `json:"module,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` + AuthenticationInfo AuditAuthenticationInfo `json:"authenticationInfo,omitempty"` + ResourceScope AuditResourceScope `json:"resourceScope,omitempty"` +} + +type AuditResource struct { + Type string `json:"type"` + Identifier string `json:"identifier"` + Labels AuditResourceLabels `json:"labels,omitempty"` +} + +type AuditResourceLabels struct { + ResourceName string `json:"resourceName,omitempty"` +} + +type AuditAuthenticationInfo struct { + Principal AuditPrincipal `json:"principal"` + Labels AuditAuthenticationInfoLabels `json:"labels,omitempty"` +} + +type AuditAuthenticationInfoLabels struct { + UserID string `json:"userId,omitempty"` + Username string `json:"username,omitempty"` +} diff --git a/pkg/harness/audit.go b/pkg/harness/audit.go new file mode 100644 index 00000000..716f348c --- /dev/null +++ b/pkg/harness/audit.go @@ -0,0 +1,91 @@ +package harness + +import ( + "context" + "encoding/json" + "fmt" + "github.com/harness/harness-mcp/client" + // "github.com/harness/harness-mcp/client/dto" + "github.com/harness/harness-mcp/cmd/harness-mcp-server/config" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" + "time" +) + +const ( + minPage = 0 + maxPage = 100 + minSize = 1 + maxSize = 100 +) + +func maxMin(val, min, max int) int { + if(val < min) { + return min + } + if(val > max) { + return max + } + return val +} + +// ListUserPipelineRunsTool creates a tool for listing all pipeline IDs run by a specific user. +func ListUserPipelineRunsTool(config *config.Config, auditClient *client.AuditService) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("list_user_audits", + mcp.WithDescription("List the audit trail of the user."), + mcp.WithString("user_id", + mcp.Required(), + mcp.Description("The user id used to retrieve the audit trail."), + ), + mcp.WithNumber("start_time", + mcp.Description("Optional start time in milliseconds"), + mcp.DefaultNumber(0), + ), + mcp.WithNumber("end_time", + mcp.Description("Optional end time in milliseconds"), + mcp.DefaultNumber(float64(time.Now().UnixMilli())), + ), + WithScope(config, true), + WithPagination(), + ), + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + userID, err := requiredParam[string](request, "user_id") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + scope, err := fetchScope(config, request, true) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + page, size, err := fetchPagination(request) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + page = maxMin(page, minPage, maxPage) + size = maxMin(size, minSize, maxSize) + + startTime, _ := OptionalParam[int64](request, "start_time") + endTime, _ := OptionalParam[int64](request, "end_time") + + toGetModules := []string{"PMS", "CORE"} + modules, _ := OptionalParam[[]string](request, "modules") + if modules != nil { + toGetModules = modules + } + + data, err := auditClient.ListPipelineExecutionsByUser(ctx, scope, userID, toGetModules, page, size, startTime, endTime, nil) + if err != nil { + return nil, fmt.Errorf("failed to list the audit logs: %w", err) + } + + r, err := json.Marshal(data) + if err != nil { + return nil, fmt.Errorf("failed to marshal the audit logs: %w", err) + } + + return mcp.NewToolResultText(string(r)), nil + } +} \ No newline at end of file From 5ba4a4d9337e674d76fc3b9596bb1362ee695f39 Mon Sep 17 00:00:00 2001 From: Abhishek Singh <126397507+abhishek-singh0710@users.noreply.github.com> Date: Fri, 4 Jul 2025 16:42:05 +0530 Subject: [PATCH 02/11] Update audit.go Updated the function identifier. --- client/audit.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/client/audit.go b/client/audit.go index 3bfae193..b66e0bcf 100644 --- a/client/audit.go +++ b/client/audit.go @@ -10,14 +10,14 @@ type AuditService struct { Client *Client } -// ListPipelineExecutionsByUser fetches pipeline executions triggered by a specific user. -func (a *AuditService) ListPipelineExecutionsByUser(ctx context.Context, scope dto.Scope, userID string, modules []string, page int, size int, startTime int64, endTime int64, opts *dto.ListAuditEventsFilter) (*dto.AuditOutput[dto.AuditListItem], error) { +// ListUserAuditTrail fetches the audit trail of a specific user. +func (a *AuditService) ListUserAuditTrail(ctx context.Context, scope dto.Scope, userID string, page int, size int, startTime int64, endTime int64, opts *dto.ListAuditEventsFilter) (*dto.AuditOutput[dto.AuditListItem], error) { if opts == nil { opts = &dto.ListAuditEventsFilter{} } params := make(map[string]string) - params["routingId"] = scope.AccountID // required for Harness gateway APIs + params["routingId"] = scope.AccountID params["accountIdentifier"] = scope.AccountID params["pageIndex"] = fmt.Sprintf("%d", page) params["pageSize"] = fmt.Sprintf("%d", size) @@ -37,8 +37,7 @@ func (a *AuditService) ListPipelineExecutionsByUser(ctx context.Context, scope d ProjectIdentifier: scope.ProjectID, }} - opts.Modules = modules - opts.StartTime = startTime // or use a date range with UnixMillis + opts.StartTime = startTime opts.EndTime = endTime resp := &dto.AuditOutput[dto.AuditListItem]{} @@ -48,4 +47,4 @@ func (a *AuditService) ListPipelineExecutionsByUser(ctx context.Context, scope d } return resp, nil -} \ No newline at end of file +} From 3d82f2ec955018853d712412ddd6beb26e620cc4 Mon Sep 17 00:00:00 2001 From: Abhishek Singh <126397507+abhishek-singh0710@users.noreply.github.com> Date: Fri, 4 Jul 2025 16:43:39 +0530 Subject: [PATCH 03/11] Update pkg/harness.audit.go Updated function identifier. --- pkg/harness/audit.go | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/pkg/harness/audit.go b/pkg/harness/audit.go index 716f348c..b003e09f 100644 --- a/pkg/harness/audit.go +++ b/pkg/harness/audit.go @@ -29,8 +29,8 @@ func maxMin(val, min, max int) int { return val } -// ListUserPipelineRunsTool creates a tool for listing all pipeline IDs run by a specific user. -func ListUserPipelineRunsTool(config *config.Config, auditClient *client.AuditService) (tool mcp.Tool, handler server.ToolHandlerFunc) { +// ListAuditsOfUser creates a tool for listing the audit trail of a specific user. +func ListUserAuditTrail(config *config.Config, auditClient *client.AuditService) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("list_user_audits", mcp.WithDescription("List the audit trail of the user."), mcp.WithString("user_id", @@ -70,13 +70,7 @@ func ListUserPipelineRunsTool(config *config.Config, auditClient *client.AuditSe startTime, _ := OptionalParam[int64](request, "start_time") endTime, _ := OptionalParam[int64](request, "end_time") - toGetModules := []string{"PMS", "CORE"} - modules, _ := OptionalParam[[]string](request, "modules") - if modules != nil { - toGetModules = modules - } - - data, err := auditClient.ListPipelineExecutionsByUser(ctx, scope, userID, toGetModules, page, size, startTime, endTime, nil) + data, err := auditClient.ListUserAuditTrail(ctx, scope, userID, page, size, startTime, endTime, nil) if err != nil { return nil, fmt.Errorf("failed to list the audit logs: %w", err) } @@ -88,4 +82,4 @@ func ListUserPipelineRunsTool(config *config.Config, auditClient *client.AuditSe return mcp.NewToolResultText(string(r)), nil } -} \ No newline at end of file +} From 0f7928d9c1a0aefeb6ffac928329cef3b5b5dc2c Mon Sep 17 00:00:00 2001 From: Abhishek Singh <126397507+abhishek-singh0710@users.noreply.github.com> Date: Fri, 4 Jul 2025 16:45:43 +0530 Subject: [PATCH 04/11] Update audit.go --- pkg/harness/audit.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/harness/audit.go b/pkg/harness/audit.go index b003e09f..e9be20a0 100644 --- a/pkg/harness/audit.go +++ b/pkg/harness/audit.go @@ -29,7 +29,7 @@ func maxMin(val, min, max int) int { return val } -// ListAuditsOfUser creates a tool for listing the audit trail of a specific user. +// ListUserAuditTrail creates a tool for listing the audit trail of a specific user. func ListUserAuditTrail(config *config.Config, auditClient *client.AuditService) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("list_user_audits", mcp.WithDescription("List the audit trail of the user."), From cc396033aee8d535001991ed951b89693e37d16d Mon Sep 17 00:00:00 2001 From: Abhishek Singh <126397507+abhishek-singh0710@users.noreply.github.com> Date: Mon, 7 Jul 2025 20:35:11 +0530 Subject: [PATCH 05/11] Update audit.go --- client/audit.go | 72 ++++++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/client/audit.go b/client/audit.go index b66e0bcf..cd4a8f36 100644 --- a/client/audit.go +++ b/client/audit.go @@ -1,50 +1,54 @@ package client import ( - "fmt" "context" + "fmt" + "github.com/harness/harness-mcp/client/dto" ) +const ( + auditPath = "gateway/audit/api/audits/list" +) + type AuditService struct { Client *Client } -// ListUserAuditTrail fetches the audit trail of a specific user. +// ListUserAuditTrail fetches the audit trail. func (a *AuditService) ListUserAuditTrail(ctx context.Context, scope dto.Scope, userID string, page int, size int, startTime int64, endTime int64, opts *dto.ListAuditEventsFilter) (*dto.AuditOutput[dto.AuditListItem], error) { - if opts == nil { - opts = &dto.ListAuditEventsFilter{} - } - - params := make(map[string]string) - params["routingId"] = scope.AccountID - params["accountIdentifier"] = scope.AccountID - params["pageIndex"] = fmt.Sprintf("%d", page) - params["pageSize"] = fmt.Sprintf("%d", size) - - addScope(scope, params) - - // Required fields - opts.FilterType = "Audit" - opts.Principals = []dto.AuditPrincipal{{ - Type: "USER", - Identifier: userID, - }} - - opts.Scopes = []dto.AuditResourceScope{{ - AccountIdentifier: scope.AccountID, - OrgIdentifier: scope.OrgID, - ProjectIdentifier: scope.ProjectID, - }} - - opts.StartTime = startTime + if opts == nil { + opts = &dto.ListAuditEventsFilter{} + } + + params := make(map[string]string) + params["accountIdentifier"] = scope.AccountID + params["pageIndex"] = fmt.Sprintf("%d", page) + params["pageSize"] = fmt.Sprintf("%d", size) + + addScope(scope, params) + + // Required fields + opts.FilterType = "Audit" + opts.Principals = []dto.AuditPrincipal{{ + Type: "USER", + Identifier: userID, + }} + + opts.Scopes = []dto.AuditResourceScope{{ + AccountIdentifier: scope.AccountID, + OrgIdentifier: scope.OrgID, + ProjectIdentifier: scope.ProjectID, + }} + + opts.StartTime = startTime // or use a date range with UnixMillis opts.EndTime = endTime - resp := &dto.AuditOutput[dto.AuditListItem]{} - err := a.Client.Post(ctx, "gateway/audit/api/audits/list", params, opts, resp) - if err != nil { - return nil, fmt.Errorf("failed to list pipeline executions by user: %w", err) - } + resp := &dto.AuditOutput[dto.AuditListItem]{} + err := a.Client.Post(ctx, auditPath, params, opts, resp) + if err != nil { + return nil, fmt.Errorf("failed to list the audit trail: %w", err) + } - return resp, nil + return resp, nil } From 17cafba53b33a14ce34d0f3cfbe35086e65c80bd Mon Sep 17 00:00:00 2001 From: Abhishek Singh <126397507+abhishek-singh0710@users.noreply.github.com> Date: Mon, 7 Jul 2025 20:36:18 +0530 Subject: [PATCH 06/11] Update audit.go --- client/dto/audit.go | 71 ++++++++++++++++++--------------------------- 1 file changed, 29 insertions(+), 42 deletions(-) diff --git a/client/dto/audit.go b/client/dto/audit.go index 4fc7d413..cc94502c 100644 --- a/client/dto/audit.go +++ b/client/dto/audit.go @@ -5,17 +5,6 @@ type AuditPrincipal struct { Identifier string `json:"identifier"` } -type AuditPaginationOptions struct { - Page int `json:"page,omitempty"` - Size int `json:"size,omitempty"` -} - -// AuditListOptions represents the options for listing pipelines -type AuditListOptions struct { - AuditPaginationOptions - SearchTerm string `json:"searchTerm,omitempty"` -} - type AuditResourceScope struct { AccountIdentifier string `json:"accountIdentifier"` OrgIdentifier string `json:"orgIdentifier,omitempty"` @@ -26,56 +15,54 @@ type ListAuditEventsFilter struct { Scopes []AuditResourceScope `json:"scopes,omitempty"` Principals []AuditPrincipal `json:"principals,omitempty"` Actions []string `json:"actions,omitempty"` - FilterType string `json:"filterType,omitempty"` - StartTime int64 `json:"startTime,omitempty"` - EndTime int64 `json:"endTime,omitempty"` - Modules []string `json:"modules,omitempty"` + FilterType string `json:"filterType,omitempty"` + StartTime int64 `json:"startTime,omitempty"` + EndTime int64 `json:"endTime,omitempty"` } - type AuditOutput[T any] struct { - Status string `json:"status,omitempty"` - Data AuditOutputData[T] `json:"data,omitempty"` + Status string `json:"status,omitempty"` + Data AuditOutputData[T] `json:"data,omitempty"` } type AuditOutputData[T any] struct { - PageItemCount int `json:"pageItemCount,omitempty"` - PageSize int `json:"pageSize,omitempty"` - Content []T `json:"content,omitempty"` - PageIndex int `json:"pageIndex,omitempty"` - HasNext bool `json:"hasNext,omitempty"` - PageToken string `json:"pageToken,omitempty"` - TotalItems int `json:totalItems, omitempty` - TotalPages int `json:totalPages, omitempty` + PageItemCount int `json:"pageItemCount,omitempty"` + PageSize int `json:"pageSize,omitempty"` + Content []T `json:"content,omitempty"` + PageIndex int `json:"pageIndex,omitempty"` + HasNext bool `json:"hasNext,omitempty"` + PageToken string `json:"pageToken,omitempty"` + TotalItems int `json:totalItems, omitempty` + TotalPages int `json:totalPages, omitempty` } type AuditListItem struct { - AuditID string `json:"auditId,omitempty"` - InsertId string `json:"insertId,omitempty"` - Resource AuditResource `json:"resource,omitempty"` - Action string `json:"action,omitempty"` - Module string `json:"module,omitempty"` - Timestamp int64 `json:"timestamp,omitempty"` - AuthenticationInfo AuditAuthenticationInfo `json:"authenticationInfo,omitempty"` - ResourceScope AuditResourceScope `json:"resourceScope,omitempty"` + AuditID string `json:"auditId,omitempty"` + InsertId string `json:"insertId,omitempty"` + Resource AuditResource `json:"resource,omitempty"` + Action string `json:"action,omitempty"` + Module string `json:"module,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` + AuthenticationInfo AuditAuthenticationInfo `json:"authenticationInfo,omitempty"` + ResourceScope AuditResourceScope `json:"resourceScope,omitempty"` } type AuditResource struct { - Type string `json:"type"` - Identifier string `json:"identifier"` - Labels AuditResourceLabels `json:"labels,omitempty"` + Type string `json:"type"` + Identifier string `json:"identifier"` + Labels AuditResourceLabels `json:"labels,omitempty"` } type AuditResourceLabels struct { - ResourceName string `json:"resourceName,omitempty"` + ResourceName string `json:"resourceName,omitempty"` } type AuditAuthenticationInfo struct { - Principal AuditPrincipal `json:"principal"` - Labels AuditAuthenticationInfoLabels `json:"labels,omitempty"` + Principal AuditPrincipal `json:"principal"` + Labels AuditAuthenticationInfoLabels `json:"labels,omitempty"` } type AuditAuthenticationInfoLabels struct { - UserID string `json:"userId,omitempty"` - Username string `json:"username,omitempty"` + UserID string `json:"userId,omitempty"` + Username string `json:"username,omitempty"` } From 01124026aec5dd4dfb70a80c4b9d29570a1b48cd Mon Sep 17 00:00:00 2001 From: Abhishek Singh <126397507+abhishek-singh0710@users.noreply.github.com> Date: Mon, 7 Jul 2025 20:38:08 +0530 Subject: [PATCH 07/11] Update audit.go --- pkg/harness/audit.go | 111 ++++++++++++++++++++----------------------- 1 file changed, 51 insertions(+), 60 deletions(-) diff --git a/pkg/harness/audit.go b/pkg/harness/audit.go index e9be20a0..ea7d32fa 100644 --- a/pkg/harness/audit.go +++ b/pkg/harness/audit.go @@ -4,82 +4,73 @@ import ( "context" "encoding/json" "fmt" + "math" + "time" + "github.com/harness/harness-mcp/client" - // "github.com/harness/harness-mcp/client/dto" "github.com/harness/harness-mcp/cmd/harness-mcp-server/config" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" - "time" ) const ( - minPage = 0 - maxPage = 100 - minSize = 1 - maxSize = 100 + minPage = 0 + maxPage = 1000 + minSize = 1 + maxSize = 1000 ) -func maxMin(val, min, max int) int { - if(val < min) { - return min - } - if(val > max) { - return max - } - return val -} - -// ListUserAuditTrail creates a tool for listing the audit trail of a specific user. +// ListAuditsOfUser creates a tool for listing the audit trail. func ListUserAuditTrail(config *config.Config, auditClient *client.AuditService) (tool mcp.Tool, handler server.ToolHandlerFunc) { - return mcp.NewTool("list_user_audits", - mcp.WithDescription("List the audit trail of the user."), - mcp.WithString("user_id", - mcp.Required(), - mcp.Description("The user id used to retrieve the audit trail."), - ), - mcp.WithNumber("start_time", - mcp.Description("Optional start time in milliseconds"), - mcp.DefaultNumber(0), - ), - mcp.WithNumber("end_time", - mcp.Description("Optional end time in milliseconds"), - mcp.DefaultNumber(float64(time.Now().UnixMilli())), - ), - WithScope(config, true), - WithPagination(), + return mcp.NewTool("list_user_audits", + mcp.WithDescription("List the audit trail of the user."), + mcp.WithString("user_id", + mcp.Required(), + mcp.Description("The user id used to retrieve the audit trail."), + ), + mcp.WithNumber("start_time", + mcp.Description("Optional start time in milliseconds"), + mcp.DefaultNumber(0), + ), + mcp.WithNumber("end_time", + mcp.Description("Optional end time in milliseconds"), + mcp.DefaultNumber(float64(time.Now().UnixMilli())), + ), + WithScope(config, true), + WithPagination(), ), - func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - userID, err := requiredParam[string](request, "user_id") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + userID, err := requiredParam[string](request, "user_id") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } - scope, err := fetchScope(config, request, true) - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } + scope, err := fetchScope(config, request, true) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } - page, size, err := fetchPagination(request) - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } + page, size, err := fetchPagination(request) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } - page = maxMin(page, minPage, maxPage) - size = maxMin(size, minSize, maxSize) + page = int(math.Min(math.Max(float64(page), float64(minPage)), float64(maxPage))) + size = int(math.Min(math.Max(float64(size), float64(minSize)), float64(maxSize))) - startTime, _ := OptionalParam[int64](request, "start_time") - endTime, _ := OptionalParam[int64](request, "end_time") + startTime, _ := OptionalParam[int64](request, "start_time") + endTime, _ := OptionalParam[int64](request, "end_time") - data, err := auditClient.ListUserAuditTrail(ctx, scope, userID, page, size, startTime, endTime, nil) - if err != nil { - return nil, fmt.Errorf("failed to list the audit logs: %w", err) - } + data, err := auditClient.ListUserAuditTrail(ctx, scope, userID, page, size, startTime, endTime, nil) + if err != nil { + return nil, fmt.Errorf("failed to list the audit logs: %w", err) + } - r, err := json.Marshal(data) - if err != nil { - return nil, fmt.Errorf("failed to marshal the audit logs: %w", err) - } + r, err := json.Marshal(data) + if err != nil { + return nil, fmt.Errorf("failed to marshal the audit logs: %w", err) + } - return mcp.NewToolResultText(string(r)), nil - } + return mcp.NewToolResultText(string(r)), nil + } } From d6d50abd312ee5d8797b8d1165f01bb0a4bbb8b7 Mon Sep 17 00:00:00 2001 From: Abhishek Singh <126397507+abhishek-singh0710@users.noreply.github.com> Date: Mon, 7 Jul 2025 21:42:19 +0530 Subject: [PATCH 08/11] Update audit.go --- pkg/harness/audit.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/harness/audit.go b/pkg/harness/audit.go index ea7d32fa..171acf16 100644 --- a/pkg/harness/audit.go +++ b/pkg/harness/audit.go @@ -21,7 +21,7 @@ const ( ) // ListAuditsOfUser creates a tool for listing the audit trail. -func ListUserAuditTrail(config *config.Config, auditClient *client.AuditService) (tool mcp.Tool, handler server.ToolHandlerFunc) { +func ListUserAuditTrailTool(config *config.Config, auditClient *client.AuditService) (tool mcp.Tool, handler server.ToolHandlerFunc) { return mcp.NewTool("list_user_audits", mcp.WithDescription("List the audit trail of the user."), mcp.WithString("user_id", From 5abbe9196942929560c3004d8094dad9d8dcefe1 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Mon, 7 Jul 2025 21:50:01 +0530 Subject: [PATCH 09/11] Added tools.go file --- pkg/harness/tools.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pkg/harness/tools.go b/pkg/harness/tools.go index 3dbd0d20..9f220307 100644 --- a/pkg/harness/tools.go +++ b/pkg/harness/tools.go @@ -87,6 +87,10 @@ func InitToolsets(config *config.Config) (*toolsets.ToolsetGroup, error) { return nil, err } + if err := registerAudit(config, tsg); err != nil { + return nil, err + } + // Enable requested toolsets if err := tsg.EnableToolsets(config.Toolsets); err != nil { return nil, err @@ -541,3 +545,17 @@ func registerDashboards(config *config.Config, tsg *toolsets.ToolsetGroup) error tsg.AddToolset(dashboards) return nil } + +func registerAudit(config *config.Config, tsg *toolsets.ToolsetGroup) error { + c, err := createClient(config.BaseURL, config, "") + if err != nil { + return err + } + auditService := &client.AuditService{Client: c} + audit := toolsets.NewToolset("audit", "Audit log related tools"). + AddReadTools( + toolsets.NewServerTool(ListUserAuditTrailTool(config, auditService)), + ) + tsg.AddToolset(audit) + return nil +} From 8cb2bc30ead8d2761ddd9a5ff68b56cdada87f2f Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Mon, 7 Jul 2025 21:54:06 +0530 Subject: [PATCH 10/11] Committing local changes --- client/audit.go | 77 +++++++++++---------- client/dto/audit.go | 71 ++++++++----------- cmd/harness-mcp-server/main.go | 2 + pkg/harness/audit.go | 121 +++++++++++++++------------------ prompts.txt | 57 ++++++++++++++++ 5 files changed, 181 insertions(+), 147 deletions(-) create mode 100644 prompts.txt diff --git a/client/audit.go b/client/audit.go index 3bfae193..cd4a8f36 100644 --- a/client/audit.go +++ b/client/audit.go @@ -1,51 +1,54 @@ package client import ( - "fmt" "context" + "fmt" + "github.com/harness/harness-mcp/client/dto" ) +const ( + auditPath = "gateway/audit/api/audits/list" +) + type AuditService struct { Client *Client } -// ListPipelineExecutionsByUser fetches pipeline executions triggered by a specific user. -func (a *AuditService) ListPipelineExecutionsByUser(ctx context.Context, scope dto.Scope, userID string, modules []string, page int, size int, startTime int64, endTime int64, opts *dto.ListAuditEventsFilter) (*dto.AuditOutput[dto.AuditListItem], error) { - if opts == nil { - opts = &dto.ListAuditEventsFilter{} - } - - params := make(map[string]string) - params["routingId"] = scope.AccountID // required for Harness gateway APIs - params["accountIdentifier"] = scope.AccountID - params["pageIndex"] = fmt.Sprintf("%d", page) - params["pageSize"] = fmt.Sprintf("%d", size) - - addScope(scope, params) - - // Required fields - opts.FilterType = "Audit" - opts.Principals = []dto.AuditPrincipal{{ - Type: "USER", - Identifier: userID, - }} - - opts.Scopes = []dto.AuditResourceScope{{ - AccountIdentifier: scope.AccountID, - OrgIdentifier: scope.OrgID, - ProjectIdentifier: scope.ProjectID, - }} - - opts.Modules = modules - opts.StartTime = startTime // or use a date range with UnixMillis +// ListUserAuditTrail fetches the audit trail. +func (a *AuditService) ListUserAuditTrail(ctx context.Context, scope dto.Scope, userID string, page int, size int, startTime int64, endTime int64, opts *dto.ListAuditEventsFilter) (*dto.AuditOutput[dto.AuditListItem], error) { + if opts == nil { + opts = &dto.ListAuditEventsFilter{} + } + + params := make(map[string]string) + params["accountIdentifier"] = scope.AccountID + params["pageIndex"] = fmt.Sprintf("%d", page) + params["pageSize"] = fmt.Sprintf("%d", size) + + addScope(scope, params) + + // Required fields + opts.FilterType = "Audit" + opts.Principals = []dto.AuditPrincipal{{ + Type: "USER", + Identifier: userID, + }} + + opts.Scopes = []dto.AuditResourceScope{{ + AccountIdentifier: scope.AccountID, + OrgIdentifier: scope.OrgID, + ProjectIdentifier: scope.ProjectID, + }} + + opts.StartTime = startTime // or use a date range with UnixMillis opts.EndTime = endTime - resp := &dto.AuditOutput[dto.AuditListItem]{} - err := a.Client.Post(ctx, "gateway/audit/api/audits/list", params, opts, resp) - if err != nil { - return nil, fmt.Errorf("failed to list pipeline executions by user: %w", err) - } + resp := &dto.AuditOutput[dto.AuditListItem]{} + err := a.Client.Post(ctx, auditPath, params, opts, resp) + if err != nil { + return nil, fmt.Errorf("failed to list the audit trail: %w", err) + } - return resp, nil -} \ No newline at end of file + return resp, nil +} diff --git a/client/dto/audit.go b/client/dto/audit.go index 4fc7d413..cc94502c 100644 --- a/client/dto/audit.go +++ b/client/dto/audit.go @@ -5,17 +5,6 @@ type AuditPrincipal struct { Identifier string `json:"identifier"` } -type AuditPaginationOptions struct { - Page int `json:"page,omitempty"` - Size int `json:"size,omitempty"` -} - -// AuditListOptions represents the options for listing pipelines -type AuditListOptions struct { - AuditPaginationOptions - SearchTerm string `json:"searchTerm,omitempty"` -} - type AuditResourceScope struct { AccountIdentifier string `json:"accountIdentifier"` OrgIdentifier string `json:"orgIdentifier,omitempty"` @@ -26,56 +15,54 @@ type ListAuditEventsFilter struct { Scopes []AuditResourceScope `json:"scopes,omitempty"` Principals []AuditPrincipal `json:"principals,omitempty"` Actions []string `json:"actions,omitempty"` - FilterType string `json:"filterType,omitempty"` - StartTime int64 `json:"startTime,omitempty"` - EndTime int64 `json:"endTime,omitempty"` - Modules []string `json:"modules,omitempty"` + FilterType string `json:"filterType,omitempty"` + StartTime int64 `json:"startTime,omitempty"` + EndTime int64 `json:"endTime,omitempty"` } - type AuditOutput[T any] struct { - Status string `json:"status,omitempty"` - Data AuditOutputData[T] `json:"data,omitempty"` + Status string `json:"status,omitempty"` + Data AuditOutputData[T] `json:"data,omitempty"` } type AuditOutputData[T any] struct { - PageItemCount int `json:"pageItemCount,omitempty"` - PageSize int `json:"pageSize,omitempty"` - Content []T `json:"content,omitempty"` - PageIndex int `json:"pageIndex,omitempty"` - HasNext bool `json:"hasNext,omitempty"` - PageToken string `json:"pageToken,omitempty"` - TotalItems int `json:totalItems, omitempty` - TotalPages int `json:totalPages, omitempty` + PageItemCount int `json:"pageItemCount,omitempty"` + PageSize int `json:"pageSize,omitempty"` + Content []T `json:"content,omitempty"` + PageIndex int `json:"pageIndex,omitempty"` + HasNext bool `json:"hasNext,omitempty"` + PageToken string `json:"pageToken,omitempty"` + TotalItems int `json:totalItems, omitempty` + TotalPages int `json:totalPages, omitempty` } type AuditListItem struct { - AuditID string `json:"auditId,omitempty"` - InsertId string `json:"insertId,omitempty"` - Resource AuditResource `json:"resource,omitempty"` - Action string `json:"action,omitempty"` - Module string `json:"module,omitempty"` - Timestamp int64 `json:"timestamp,omitempty"` - AuthenticationInfo AuditAuthenticationInfo `json:"authenticationInfo,omitempty"` - ResourceScope AuditResourceScope `json:"resourceScope,omitempty"` + AuditID string `json:"auditId,omitempty"` + InsertId string `json:"insertId,omitempty"` + Resource AuditResource `json:"resource,omitempty"` + Action string `json:"action,omitempty"` + Module string `json:"module,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` + AuthenticationInfo AuditAuthenticationInfo `json:"authenticationInfo,omitempty"` + ResourceScope AuditResourceScope `json:"resourceScope,omitempty"` } type AuditResource struct { - Type string `json:"type"` - Identifier string `json:"identifier"` - Labels AuditResourceLabels `json:"labels,omitempty"` + Type string `json:"type"` + Identifier string `json:"identifier"` + Labels AuditResourceLabels `json:"labels,omitempty"` } type AuditResourceLabels struct { - ResourceName string `json:"resourceName,omitempty"` + ResourceName string `json:"resourceName,omitempty"` } type AuditAuthenticationInfo struct { - Principal AuditPrincipal `json:"principal"` - Labels AuditAuthenticationInfoLabels `json:"labels,omitempty"` + Principal AuditPrincipal `json:"principal"` + Labels AuditAuthenticationInfoLabels `json:"labels,omitempty"` } type AuditAuthenticationInfoLabels struct { - UserID string `json:"userId,omitempty"` - Username string `json:"username,omitempty"` + UserID string `json:"userId,omitempty"` + Username string `json:"username,omitempty"` } diff --git a/cmd/harness-mcp-server/main.go b/cmd/harness-mcp-server/main.go index d6959dd4..709654b9 100644 --- a/cmd/harness-mcp-server/main.go +++ b/cmd/harness-mcp-server/main.go @@ -326,6 +326,8 @@ func runStdioServer(ctx context.Context, config config.Config) error { } func main() { + fmt.Println("hello") + fmt.Println("world") if err := rootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1) diff --git a/pkg/harness/audit.go b/pkg/harness/audit.go index 716f348c..171acf16 100644 --- a/pkg/harness/audit.go +++ b/pkg/harness/audit.go @@ -4,88 +4,73 @@ import ( "context" "encoding/json" "fmt" + "math" + "time" + "github.com/harness/harness-mcp/client" - // "github.com/harness/harness-mcp/client/dto" "github.com/harness/harness-mcp/cmd/harness-mcp-server/config" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" - "time" ) const ( - minPage = 0 - maxPage = 100 - minSize = 1 - maxSize = 100 + minPage = 0 + maxPage = 1000 + minSize = 1 + maxSize = 1000 ) -func maxMin(val, min, max int) int { - if(val < min) { - return min - } - if(val > max) { - return max - } - return val -} - -// ListUserPipelineRunsTool creates a tool for listing all pipeline IDs run by a specific user. -func ListUserPipelineRunsTool(config *config.Config, auditClient *client.AuditService) (tool mcp.Tool, handler server.ToolHandlerFunc) { - return mcp.NewTool("list_user_audits", - mcp.WithDescription("List the audit trail of the user."), - mcp.WithString("user_id", - mcp.Required(), - mcp.Description("The user id used to retrieve the audit trail."), - ), - mcp.WithNumber("start_time", - mcp.Description("Optional start time in milliseconds"), - mcp.DefaultNumber(0), +// ListAuditsOfUser creates a tool for listing the audit trail. +func ListUserAuditTrailTool(config *config.Config, auditClient *client.AuditService) (tool mcp.Tool, handler server.ToolHandlerFunc) { + return mcp.NewTool("list_user_audits", + mcp.WithDescription("List the audit trail of the user."), + mcp.WithString("user_id", + mcp.Required(), + mcp.Description("The user id used to retrieve the audit trail."), + ), + mcp.WithNumber("start_time", + mcp.Description("Optional start time in milliseconds"), + mcp.DefaultNumber(0), + ), + mcp.WithNumber("end_time", + mcp.Description("Optional end time in milliseconds"), + mcp.DefaultNumber(float64(time.Now().UnixMilli())), + ), + WithScope(config, true), + WithPagination(), ), - mcp.WithNumber("end_time", - mcp.Description("Optional end time in milliseconds"), - mcp.DefaultNumber(float64(time.Now().UnixMilli())), - ), - WithScope(config, true), - WithPagination(), - ), - func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - userID, err := requiredParam[string](request, "user_id") - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } + func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + userID, err := requiredParam[string](request, "user_id") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } - scope, err := fetchScope(config, request, true) - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } + scope, err := fetchScope(config, request, true) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } - page, size, err := fetchPagination(request) - if err != nil { - return mcp.NewToolResultError(err.Error()), nil - } + page, size, err := fetchPagination(request) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } - page = maxMin(page, minPage, maxPage) - size = maxMin(size, minSize, maxSize) + page = int(math.Min(math.Max(float64(page), float64(minPage)), float64(maxPage))) + size = int(math.Min(math.Max(float64(size), float64(minSize)), float64(maxSize))) - startTime, _ := OptionalParam[int64](request, "start_time") - endTime, _ := OptionalParam[int64](request, "end_time") + startTime, _ := OptionalParam[int64](request, "start_time") + endTime, _ := OptionalParam[int64](request, "end_time") - toGetModules := []string{"PMS", "CORE"} - modules, _ := OptionalParam[[]string](request, "modules") - if modules != nil { - toGetModules = modules - } + data, err := auditClient.ListUserAuditTrail(ctx, scope, userID, page, size, startTime, endTime, nil) + if err != nil { + return nil, fmt.Errorf("failed to list the audit logs: %w", err) + } - data, err := auditClient.ListPipelineExecutionsByUser(ctx, scope, userID, toGetModules, page, size, startTime, endTime, nil) - if err != nil { - return nil, fmt.Errorf("failed to list the audit logs: %w", err) - } + r, err := json.Marshal(data) + if err != nil { + return nil, fmt.Errorf("failed to marshal the audit logs: %w", err) + } - r, err := json.Marshal(data) - if err != nil { - return nil, fmt.Errorf("failed to marshal the audit logs: %w", err) - } - - return mcp.NewToolResultText(string(r)), nil - } -} \ No newline at end of file + return mcp.NewToolResultText(string(r)), nil + } +} diff --git a/prompts.txt b/prompts.txt new file mode 100644 index 00000000..9106eee4 --- /dev/null +++ b/prompts.txt @@ -0,0 +1,57 @@ + + +### 1. Audit Search Logic + +- When the user requests a specific audit or action **without specifying a page number or size**: + - Use the default page size. + - If no matching audit is found, increment the page size by 10 and continue searching. + - Before continuing with further searches, ask the user for permission to keep checking. + +--- + +### 2. Output Formatting + +- For all audit outputs: + - Provide results in **both JSON format and tabular format**. + - Ensure every entry includes timestamps, unless the user specifically requests to exclude certain entries. + +--- + +### 3. Date to Unix Milliseconds Conversion + +- When fetching audits with a filter for start time or end time, or when converting any date to a Unix milliseconds timestamp: + 1. **Parse the prompt** to extract: + - Year, month, day, and hour. + - If minutes, seconds, or nanoseconds are not provided, default them to 0. + 2. **Use the following Go code template** to perform the conversion: + ```go + loc, _ := time.LoadLocation({location}) + t := time.Date({year}, time.{month}, {day}, {hour}, {minute}, {second}, {nanosecond}, loc) + ``` + - Replace `{location}` with the value provided by the user ("Asia/Kolkata" or "UTC"). + - Replace other placeholders with the extracted date/time values. + 3. **Important:** + - Do **not** adjust the time for any location; use the time as given for the specified location. + - Only use the location for the `{location}` parameter in the code. + +- **Location Selection:** + - Ask the user to specify the location. + - Available options: `"Asia/Kolkata"`, `"UTC"`. + +--- + +### 4. Go time.Date Function Reference + +- The format for the Go `time.Date` function is: + ```go + time.Date( + year int, + month time.Month, + day int, + hour int, + minute int, + second int, + nanosecond int, + location *time.Location, + ) + ``` From b7c60b1b66769b2828ee62595b3ee2196132aab8 Mon Sep 17 00:00:00 2001 From: Abhishek Singh <126397507+abhishek-singh0710@users.noreply.github.com> Date: Mon, 7 Jul 2025 22:03:37 +0530 Subject: [PATCH 11/11] Removed all the logging statements. --- cmd/harness-mcp-server/main.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/cmd/harness-mcp-server/main.go b/cmd/harness-mcp-server/main.go index eba5afef..9cae7f65 100644 --- a/cmd/harness-mcp-server/main.go +++ b/cmd/harness-mcp-server/main.go @@ -328,8 +328,6 @@ func runStdioServer(ctx context.Context, config config.Config) error { } func main() { - fmt.Println("hello") - fmt.Println("world") if err := rootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1)