Skip to content

Commit 70c6966

Browse files
abhishekSingh1007Harness
authored andcommitted
feat: [PL-63942]: Implement MCP Server Tool for Audit Trail. (#31)
* fix: [PL-63942]: Removed unused imports * fix: [PL-63942]: Reused Date Conversion Method * fix: [PL-63942]: Reused Date Conversion Method * fix: [PL-63942]: Resolved Conflicts * fix: [PL-63942]: Added Time in the Response * fix: [PL-63942]: Registered The Audit Tool * fix: [PL-63942]: Removed Comments * fix: [PL-63942]: Added Internal Mode Support, Date/Time Filter * fix: [PL-63942]: Updated README.md * fix: [PL-63942]: Updated README.md, resolved conflicts * feat: [PL-63942]: Added Audit Trail Tool.
1 parent 2791981 commit 70c6966

File tree

8 files changed

+296
-1
lines changed

8 files changed

+296
-1
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,13 @@ Toolset Name: `idp`
142142
- `get_score_summary`: Get Score Summary for Scorecards in the Harness Internal Developer Portal Catalog.
143143
- `get_scores`: Get Scores for Scorecards in the Harness Internal Developer Portal Catalog.
144144

145+
#### Audit Trail Toolset
146+
147+
Toolset Name: `audit`
148+
149+
- `list_user_audits`: Retrieve the complete audit trail for a specified user.
150+
151+
145152
## Prerequisites
146153

147154
1. You will need to have Go 1.23 or later installed on your system.

client/audit.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package client
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/harness/harness-mcp/client/dto"
8+
)
9+
10+
const (
11+
auditPath = "/api/audits/list"
12+
)
13+
14+
type AuditService struct {
15+
Client *Client
16+
}
17+
18+
// ListUserAuditTrail fetches the audit trail.
19+
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) {
20+
if opts == nil {
21+
opts = &dto.ListAuditEventsFilter{}
22+
}
23+
24+
params := make(map[string]string)
25+
params["accountIdentifier"] = scope.AccountID
26+
params["pageIndex"] = fmt.Sprintf("%d", page)
27+
params["pageSize"] = fmt.Sprintf("%d", size)
28+
29+
addScope(scope, params)
30+
31+
// Required fields
32+
opts.FilterType = "Audit"
33+
opts.Principals = []dto.AuditPrincipal{{
34+
Type: "USER",
35+
Identifier: userID,
36+
}}
37+
38+
opts.Scopes = []dto.AuditResourceScope{{
39+
AccountIdentifier: scope.AccountID,
40+
OrgIdentifier: scope.OrgID,
41+
ProjectIdentifier: scope.ProjectID,
42+
}}
43+
44+
opts.StartTime = startTime
45+
opts.EndTime = endTime
46+
47+
resp := &dto.AuditOutput[dto.AuditListItem]{}
48+
err := a.Client.Post(ctx, auditPath, params, opts, resp)
49+
if err != nil {
50+
return nil, fmt.Errorf("failed to list the audit trail: %w", err)
51+
}
52+
53+
for i := range resp.Data.Content {
54+
timestamp := resp.Data.Content[i].Timestamp
55+
resp.Data.Content[i].Time = dto.FormatUnixMillisToRFC3339(timestamp)
56+
}
57+
58+
return resp, nil
59+
}

client/dto/audit.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package dto
2+
3+
type AuditPrincipal struct {
4+
Type string `json:"type"`
5+
Identifier string `json:"identifier"`
6+
}
7+
8+
type AuditResourceScope struct {
9+
AccountIdentifier string `json:"accountIdentifier"`
10+
OrgIdentifier string `json:"orgIdentifier,omitempty"`
11+
ProjectIdentifier string `json:"projectIdentifier,omitempty"`
12+
}
13+
14+
type ListAuditEventsFilter struct {
15+
Scopes []AuditResourceScope `json:"scopes,omitempty"`
16+
Principals []AuditPrincipal `json:"principals,omitempty"`
17+
Actions []string `json:"actions,omitempty"`
18+
FilterType string `json:"filterType,omitempty"`
19+
StartTime int64 `json:"startTime,omitempty"`
20+
EndTime int64 `json:"endTime,omitempty"`
21+
}
22+
23+
type AuditOutput[T any] struct {
24+
Status string `json:"status,omitempty"`
25+
Data AuditOutputData[T] `json:"data,omitempty"`
26+
}
27+
28+
type AuditOutputData[T any] struct {
29+
PageItemCount int `json:"pageItemCount,omitempty"`
30+
PageSize int `json:"pageSize,omitempty"`
31+
Content []T `json:"content,omitempty"`
32+
PageIndex int `json:"pageIndex,omitempty"`
33+
HasNext bool `json:"hasNext,omitempty"`
34+
PageToken string `json:"pageToken,omitempty"`
35+
TotalItems int `json:totalItems, omitempty`
36+
TotalPages int `json:totalPages, omitempty`
37+
}
38+
39+
type AuditListItem struct {
40+
AuditID string `json:"auditId,omitempty"`
41+
InsertId string `json:"insertId,omitempty"`
42+
Resource AuditResource `json:"resource,omitempty"`
43+
Action string `json:"action,omitempty"`
44+
Module string `json:"module,omitempty"`
45+
Timestamp int64 `json:"timestamp,omitempty"`
46+
AuthenticationInfo AuditAuthenticationInfo `json:"authenticationInfo,omitempty"`
47+
ResourceScope AuditResourceScope `json:"resourceScope,omitempty"`
48+
Time string
49+
}
50+
51+
type AuditResource struct {
52+
Type string `json:"type"`
53+
Identifier string `json:"identifier"`
54+
Labels AuditResourceLabels `json:"labels,omitempty"`
55+
}
56+
57+
type AuditResourceLabels struct {
58+
ResourceName string `json:"resourceName,omitempty"`
59+
}
60+
61+
type AuditAuthenticationInfo struct {
62+
Principal AuditPrincipal `json:"principal"`
63+
Labels AuditAuthenticationInfoLabels `json:"labels,omitempty"`
64+
}
65+
66+
type AuditAuthenticationInfoLabels struct {
67+
UserID string `json:"userId,omitempty"`
68+
Username string `json:"username,omitempty"`
69+
}

cmd/harness-mcp-server/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,6 @@ type Config struct {
4848
SCSSvcBaseURL string // Added for SCS toolset
4949
STOSvcSecret string // Added for STO toolset
5050
STOSvcBaseURL string // Added for STO toolset
51+
AuditSvcBaseURL string
52+
AuditSvcSecret string
5153
}

cmd/harness-mcp-server/main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ var (
160160
SCSSvcBaseURL: viper.GetString("scs_svc_base_url"),
161161
STOSvcSecret: viper.GetString("sto_svc_secret"),
162162
STOSvcBaseURL: viper.GetString("sto_svc_base_url"),
163+
AuditSvcBaseURL: viper.GetString("audit_svc_base_url"),
164+
AuditSvcSecret: viper.GetString("audit_svc_secret"),
163165
}
164166

165167
if err := runStdioServer(ctx, cfg); err != nil {
@@ -217,6 +219,8 @@ func init() {
217219
internalCmd.Flags().String("scs-svc-base-url", "", "Base URL for SCS service")
218220
internalCmd.Flags().String("sto-svc-secret", "", "Secret for STO service")
219221
internalCmd.Flags().String("sto-svc-base-url", "", "Base URL for STO service")
222+
internalCmd.Flags().String("audit-svc-base-url", "", "Base URL for audit service")
223+
internalCmd.Flags().String("audit-svc-secret", "", "Secret for audit service")
220224

221225
// Bind global flags to viper
222226
_ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets"))
@@ -259,6 +263,8 @@ func init() {
259263
_ = viper.BindPFlag("scs_svc_base_url", internalCmd.Flags().Lookup("scs-svc-base-url"))
260264
_ = viper.BindPFlag("sto_svc_secret", internalCmd.Flags().Lookup("sto-svc-secret"))
261265
_ = viper.BindPFlag("sto_svc_base_url", internalCmd.Flags().Lookup("sto-svc-base-url"))
266+
_ = viper.BindPFlag("audit_svc_base_url", internalCmd.Flags().Lookup("audit-svc-base-url"))
267+
_ = viper.BindPFlag("audit_svc_secret", internalCmd.Flags().Lookup("audit-svc-secret"))
262268

263269
// Add subcommands
264270
rootCmd.AddCommand(stdioCmd)

pkg/harness/audit.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package harness
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"math"
8+
"time"
9+
10+
"github.com/harness/harness-mcp/client"
11+
"github.com/harness/harness-mcp/cmd/harness-mcp-server/config"
12+
"github.com/mark3labs/mcp-go/mcp"
13+
"github.com/mark3labs/mcp-go/server"
14+
)
15+
16+
const (
17+
minPage = 0
18+
maxPage = 1000
19+
minSize = 1
20+
maxSize = 1000
21+
)
22+
23+
func convertDateToMilliseconds(timestamp string) int64 {
24+
t, err := time.Parse(time.RFC3339, timestamp)
25+
if err != nil {
26+
panic(err)
27+
}
28+
29+
year := t.Year()
30+
month := int(t.Month())
31+
day := t.Day()
32+
hour := t.Hour()
33+
minute := t.Minute()
34+
second := t.Second()
35+
36+
t = time.Date(year, time.Month(month), day, hour, minute, second, 0, time.Local)
37+
38+
// Convert to Unix milliseconds
39+
unixMillis := t.UnixNano() / int64(time.Millisecond)
40+
41+
return unixMillis
42+
43+
}
44+
45+
func getCurrentTime() string {
46+
now := time.Now().UTC().Format(time.RFC3339)
47+
return now
48+
}
49+
50+
func previousWeek() string {
51+
oneWeekAgo := time.Now().AddDate(0, 0, -7).UTC().Format(time.RFC3339)
52+
return oneWeekAgo
53+
}
54+
55+
// ListAuditsOfUser creates a tool for listing the audit trail.
56+
func ListUserAuditTrailTool(config *config.Config, auditClient *client.AuditService) (tool mcp.Tool, handler server.ToolHandlerFunc) {
57+
return mcp.NewTool("list_user_audits",
58+
mcp.WithDescription("List the audit trail of the user."),
59+
mcp.WithString("user_id",
60+
mcp.Required(),
61+
mcp.Description("The user id(emailId) used to retrieve the audit trail."),
62+
),
63+
mcp.WithString("start_time",
64+
mcp.Description("Optional start time in ISO 8601 format (e.g., '2025-07-10T08:00:00Z')"),
65+
mcp.DefaultString(previousWeek()),
66+
),
67+
mcp.WithString("end_time",
68+
mcp.Description("Optional end time in ISO 8601 format (e.g., '2025-07-10T08:00:00Z')"),
69+
mcp.DefaultString(getCurrentTime()),
70+
),
71+
WithScope(config, false),
72+
WithPagination(),
73+
),
74+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
75+
userID, err := requiredParam[string](request, "user_id")
76+
if err != nil {
77+
return mcp.NewToolResultError(err.Error()), nil
78+
}
79+
80+
scope, err := fetchScope(config, request, false)
81+
if err != nil {
82+
return mcp.NewToolResultError(err.Error()), nil
83+
}
84+
85+
page, size, err := fetchPagination(request)
86+
if err != nil {
87+
return mcp.NewToolResultError(err.Error()), nil
88+
}
89+
90+
page = int(math.Min(math.Max(float64(page), float64(minPage)), float64(maxPage)))
91+
size = int(math.Min(math.Max(float64(size), float64(minSize)), float64(maxSize)))
92+
93+
startTime, _ := OptionalParam[string](request, "start_time")
94+
endTime, _ := OptionalParam[string](request, "end_time")
95+
96+
startTimeMilliseconds := convertDateToMilliseconds(startTime)
97+
endTimeMilliseconds := convertDateToMilliseconds(endTime)
98+
99+
data, err := auditClient.ListUserAuditTrail(ctx, scope, userID, page, size, startTimeMilliseconds, endTimeMilliseconds, nil)
100+
if err != nil {
101+
return nil, fmt.Errorf("failed to list the audit logs: %w", err)
102+
}
103+
104+
r, err := json.Marshal(data)
105+
if err != nil {
106+
return nil, fmt.Errorf("failed to marshal the audit logs: %w", err)
107+
}
108+
109+
return mcp.NewToolResultText(string(r)), nil
110+
}
111+
}

pkg/harness/tools.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ func InitToolsets(config *config.Config) (*toolsets.ToolsetGroup, error) {
110110
return nil, err
111111
}
112112

113+
if err := registerAudit(config, tsg); err != nil {
114+
return nil, err
115+
}
116+
113117
// Enable requested toolsets
114118
if err := tsg.EnableToolsets(config.Toolsets); err != nil {
115119
return nil, err
@@ -122,7 +126,7 @@ func buildServiceURL(config *config.Config, internalBaseURL, externalBaseURL str
122126
if config.Internal {
123127
return internalBaseURL
124128
}
125-
return externalBaseURL + externalPathPrefix
129+
return externalBaseURL + "/" + externalPathPrefix
126130
}
127131

128132
// createClient creates a client with the appropriate authentication method based on the config
@@ -716,3 +720,22 @@ func registerInternalDeveloperPortal(config *config.Config, tsg *toolsets.Toolse
716720
tsg.AddToolset(idp)
717721
return nil
718722
}
723+
func registerAudit(config *config.Config, tsg *toolsets.ToolsetGroup) error {
724+
// Determine the base URL and secret for audit service
725+
baseURL := buildServiceURL(config, config.AuditSvcBaseURL, config.BaseURL, "audit")
726+
secret := config.AuditSvcSecret
727+
728+
c, err := createClient(baseURL, config, secret)
729+
if err != nil {
730+
return err
731+
}
732+
auditService := &client.AuditService{Client: c}
733+
audit := toolsets.NewToolset("audit", "Audit log related tools").
734+
AddReadTools(
735+
toolsets.NewServerTool(ListUserAuditTrailTool(config, auditService)),
736+
)
737+
738+
// Add toolset to the group
739+
tsg.AddToolset(audit)
740+
return nil
741+
}

prompts.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<audits>
2+
3+
### 1. Audit Search Logic
4+
5+
- When the user requests a specific audit or action **without specifying a page number or size**:
6+
- Use the default page size.
7+
- If no matching audit is found, increment the page size by 10 and continue searching.
8+
- Before continuing with further searches, ask the user for permission to keep checking.
9+
10+
---
11+
12+
### 2. Output Formatting
13+
14+
- For all audit outputs:
15+
- Provide results in **both JSON format and tabular format**.
16+
- Ensure every entry includes timestamps, unless the user specifically requests to exclude certain entries.
17+
18+
---

0 commit comments

Comments
 (0)