Skip to content

Commit deb4d85

Browse files
Saranya-jenaHarness
authored andcommitted
chore: [ML-1458]: Update license config client for internal mode in mcp server (#228)
* 1b0b81 chore: [ML-1458]: deleted unnecessary files * 989c68 chore: [ML-1458]: Update license config client for internal mode in mcp server * 9e74a4 chore: [ML-1458]: Update license config client for internal mode in mcp server
1 parent 6bc39f5 commit deb4d85

File tree

3 files changed

+340
-30
lines changed

3 files changed

+340
-30
lines changed
Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
package license
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io/ioutil"
8+
"log/slog"
9+
"net/http"
10+
"net/url"
11+
"strings"
12+
"time"
13+
14+
"github.com/harness/harness-go-sdk/harness/nextgen"
15+
"github.com/harness/harness-mcp/cmd/harness-mcp-server/config"
16+
"github.com/harness/harness-mcp/pkg/harness/auth"
17+
"github.com/hashicorp/go-retryablehttp"
18+
)
19+
20+
// CustomLicensesApiService is a custom implementation of the LicensesApiService
21+
// that allows for more customization of the client configuration
22+
type CustomLicensesApiService struct {
23+
client *CustomAPIClient
24+
}
25+
26+
// CustomAPIClient is a custom implementation of the nextgen.APIClient
27+
// that allows for more customization of the client configuration
28+
type CustomAPIClient struct {
29+
cfg *nextgen.Configuration
30+
httpClient *http.Client
31+
}
32+
33+
// NewCustomAPIClient creates a new CustomAPIClient
34+
func NewCustomAPIClient(cfg *nextgen.Configuration) *CustomAPIClient {
35+
return &CustomAPIClient{
36+
cfg: cfg,
37+
httpClient: cfg.HTTPClient.StandardClient(),
38+
}
39+
}
40+
41+
// prepareRequest builds the request
42+
func (c *CustomAPIClient) prepareRequest(
43+
ctx context.Context,
44+
path string,
45+
method string,
46+
postBody interface{},
47+
headerParams map[string]string,
48+
queryParams url.Values,
49+
formParams url.Values,
50+
fileName string,
51+
fileBytes []byte) (localVarRequest *http.Request, err error) {
52+
53+
var body *strings.Reader
54+
55+
// Detect postBody type and set body content
56+
if postBody != nil {
57+
contentType := headerParams["Content-Type"]
58+
if contentType == "" {
59+
contentType = detectContentType(postBody)
60+
headerParams["Content-Type"] = contentType
61+
}
62+
63+
body, err = setBody(postBody, contentType)
64+
if err != nil {
65+
return nil, err
66+
}
67+
}
68+
69+
// Setup path and query parameters
70+
url, err := url.Parse(path)
71+
if err != nil {
72+
return nil, err
73+
}
74+
75+
// Adding Query Param
76+
query := url.Query()
77+
for k, v := range queryParams {
78+
for _, iv := range v {
79+
query.Add(k, iv)
80+
}
81+
}
82+
83+
// Encode the parameters.
84+
url.RawQuery = query.Encode()
85+
86+
// Generate a new request
87+
if body != nil {
88+
localVarRequest, err = http.NewRequest(method, url.String(), body)
89+
} else {
90+
localVarRequest, err = http.NewRequest(method, url.String(), nil)
91+
}
92+
if err != nil {
93+
return nil, err
94+
}
95+
96+
// add header parameters, if any
97+
if len(headerParams) > 0 {
98+
headers := http.Header{}
99+
for h, v := range headerParams {
100+
headers.Set(h, v)
101+
}
102+
localVarRequest.Header = headers
103+
}
104+
105+
// Add the user agent to the request.
106+
localVarRequest.Header.Add("User-Agent", c.cfg.UserAgent)
107+
108+
if ctx != nil {
109+
// add context to the request
110+
localVarRequest = localVarRequest.WithContext(ctx)
111+
112+
// Walk through any authentication.
113+
if auth, ok := ctx.Value(nextgen.ContextAPIKey).(nextgen.APIKey); ok {
114+
var key string
115+
if auth.Prefix != "" {
116+
key = auth.Prefix + " " + auth.Key
117+
} else {
118+
key = auth.Key
119+
}
120+
localVarRequest.Header.Add("x-api-key", key)
121+
}
122+
}
123+
124+
for header, value := range c.cfg.DefaultHeader {
125+
localVarRequest.Header.Add(header, value)
126+
}
127+
128+
return localVarRequest, nil
129+
}
130+
131+
// callAPI do the request.
132+
func (c *CustomAPIClient) callAPI(request *http.Request) (*http.Response, error) {
133+
// Log request details
134+
slog.Info("Request", "method", request.Method, "url", request.URL)
135+
136+
response, err := c.httpClient.Do(request)
137+
if err != nil {
138+
return response, err
139+
}
140+
141+
// Log response details
142+
slog.Info("Response", "status", response.Status)
143+
return response, err
144+
}
145+
146+
// decode handles decoding response bodies into target types
147+
func (c *CustomAPIClient) decode(v interface{}, b []byte, contentType string) (err error) {
148+
// Use the standard JSON decoder for simplicity
149+
if strings.Contains(contentType, "application/json") {
150+
return json.Unmarshal(b, v)
151+
}
152+
// For other content types, just return the raw data
153+
return fmt.Errorf("unsupported content type: %s", contentType)
154+
}
155+
156+
// detectContentType detects the content type of the given body
157+
func detectContentType(body interface{}) string {
158+
// Default to JSON for all requests
159+
return "application/json"
160+
}
161+
162+
// setBody sets the body of the request
163+
func setBody(body interface{}, contentType string) (bodyBuf *strings.Reader, err error) {
164+
// Convert the body to JSON
165+
if body != nil {
166+
jsonData, err := json.Marshal(body)
167+
if err != nil {
168+
return nil, err
169+
}
170+
bodyBuf = strings.NewReader(string(jsonData))
171+
} else {
172+
bodyBuf = strings.NewReader("{}")
173+
}
174+
return bodyBuf, nil
175+
}
176+
177+
// selectHeaderContentType selects the best content type from the available options
178+
func selectHeaderContentType(contentTypes []string) string {
179+
if len(contentTypes) == 0 {
180+
return ""
181+
}
182+
if contains(contentTypes, "application/json") {
183+
return "application/json"
184+
}
185+
return contentTypes[0]
186+
}
187+
188+
// selectHeaderAccept selects the best accept header from the available options
189+
func selectHeaderAccept(accepts []string) string {
190+
if len(accepts) == 0 {
191+
return ""
192+
}
193+
if contains(accepts, "application/json") {
194+
return "application/json"
195+
}
196+
return accepts[0]
197+
}
198+
199+
// contains checks if a string is in a slice
200+
func contains(slice []string, item string) bool {
201+
for _, s := range slice {
202+
if s == item {
203+
return true
204+
}
205+
}
206+
return false
207+
}
208+
209+
// GetAccountLicenses gets all module license information in account
210+
func (a *CustomLicensesApiService) GetAccountLicenses(ctx context.Context, accountIdentifier string) (nextgen.ResponseDtoAccountLicense, *http.Response, error) {
211+
var (
212+
localVarHttpMethod = strings.ToUpper("Get")
213+
localVarPostBody interface{}
214+
localVarFileName string
215+
localVarFileBytes []byte
216+
localVarReturnValue nextgen.ResponseDtoAccountLicense
217+
)
218+
219+
// create path and map variables
220+
localVarPath := a.client.cfg.BasePath + "/licenses/account"
221+
222+
localVarHeaderParams := make(map[string]string)
223+
localVarQueryParams := url.Values{}
224+
localVarFormParams := url.Values{}
225+
226+
localVarQueryParams.Add("accountIdentifier", accountIdentifier)
227+
228+
// to determine the Content-Type header
229+
localVarHttpContentTypes := []string{}
230+
231+
// set Content-Type header
232+
localVarHttpContentType := selectHeaderContentType(localVarHttpContentTypes)
233+
if localVarHttpContentType != "" {
234+
localVarHeaderParams["Content-Type"] = localVarHttpContentType
235+
}
236+
237+
// to determine the Accept header
238+
localVarHttpHeaderAccepts := []string{"application/json", "application/yaml"}
239+
240+
// set Accept header
241+
localVarHttpHeaderAccept := selectHeaderAccept(localVarHttpHeaderAccepts)
242+
if localVarHttpHeaderAccept != "" {
243+
localVarHeaderParams["Accept"] = localVarHttpHeaderAccept
244+
}
245+
246+
r, err := a.client.prepareRequest(ctx, localVarPath, localVarHttpMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFileName, localVarFileBytes)
247+
if err != nil {
248+
return localVarReturnValue, nil, err
249+
}
250+
251+
localVarHttpResponse, err := a.client.callAPI(r)
252+
if err != nil || localVarHttpResponse == nil {
253+
return localVarReturnValue, localVarHttpResponse, err
254+
}
255+
256+
localVarBody, err := ioutil.ReadAll(localVarHttpResponse.Body)
257+
localVarHttpResponse.Body.Close()
258+
if err != nil {
259+
return localVarReturnValue, localVarHttpResponse, err
260+
}
261+
262+
if localVarHttpResponse.StatusCode < 300 {
263+
// If we succeed, return the data, otherwise pass on to decode error.
264+
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHttpResponse.Header.Get("Content-Type"))
265+
if err == nil {
266+
return localVarReturnValue, localVarHttpResponse, err
267+
}
268+
}
269+
270+
if localVarHttpResponse.StatusCode >= 300 {
271+
// Create a custom error with the response body and status
272+
return localVarReturnValue, localVarHttpResponse, fmt.Errorf("HTTP error %s with body: %s", localVarHttpResponse.Status, string(localVarBody))
273+
}
274+
275+
return localVarReturnValue, localVarHttpResponse, nil
276+
}
277+
278+
// CreateCustomLicenseClient creates a custom license client with the appropriate authentication method based on the config
279+
func CreateCustomLicenseClient(config *config.Config, licenseBaseURL, baseURL, path, secret string) (*CustomLicensesApiService, error) {
280+
return CreateCustomLicenseClientWithContext(context.Background(), config, licenseBaseURL, baseURL, path, secret)
281+
}
282+
283+
// CreateCustomLicenseClientWithContext creates a custom license client with the appropriate authentication method based on the config
284+
// It handles both internal and external modes with the correct authentication
285+
// The context is used for JWT authentication in internal mode
286+
func CreateCustomLicenseClientWithContext(ctx context.Context, config *config.Config, licenseBaseURL, baseURL, path, secret string) (*CustomLicensesApiService, error) {
287+
// Create a standard HTTP client
288+
httpClient := retryablehttp.NewClient()
289+
httpClient.RetryMax = 5
290+
291+
// Build the service URL based on whether we're in internal or external mode
292+
serviceURL := buildServiceURL(config, licenseBaseURL, baseURL, path)
293+
294+
cfg := nextgen.Configuration{
295+
AccountId: config.AccountID,
296+
BasePath: serviceURL,
297+
HTTPClient: httpClient,
298+
DebugLogging: true,
299+
}
300+
301+
// Set up authentication based on internal/external mode
302+
if config.Internal {
303+
// Use JWT auth for internal mode
304+
jwtProvider := auth.NewJWTProvider(secret, serviceIdentity, &defaultJWTLifetime)
305+
headerName, headerValue, err := jwtProvider.GetHeader(ctx)
306+
if err != nil {
307+
slog.Error("Failed to get JWT token", "error", err)
308+
return nil, fmt.Errorf("failed to get JWT token: %w", err)
309+
}
310+
cfg.DefaultHeader = map[string]string{headerName: headerValue}
311+
} else {
312+
// Use API key auth for external mode
313+
cfg.ApiKey = config.APIKey
314+
cfg.DefaultHeader = map[string]string{"x-api-key": config.APIKey}
315+
}
316+
317+
customClient := NewCustomAPIClient(&cfg)
318+
return &CustomLicensesApiService{client: customClient}, nil
319+
}
320+
321+
// buildServiceURL builds the service URL based on whether we're in internal or external mode
322+
func buildServiceURL(config *config.Config, internalBaseURL, externalBaseURL string, externalPathPrefix string) string {
323+
if config.Internal {
324+
return internalBaseURL
325+
}
326+
if externalPathPrefix == "" {
327+
return externalBaseURL
328+
}
329+
return externalBaseURL + "/" + externalPathPrefix
330+
}
331+
332+
// Default JWT token lifetime
333+
var defaultJWTLifetime = 1 * 60 * 60 * time.Second // 1 hour
334+
335+
var serviceIdentity = "genaiservice"

pkg/harness/tools.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"time"
1010

1111
"github.com/harness/harness-mcp/client"
12+
"github.com/harness/harness-mcp/client/license"
1213
"github.com/harness/harness-mcp/cmd/harness-mcp-server/config"
1314
"github.com/harness/harness-mcp/pkg/harness/tools"
1415
"github.com/harness/harness-mcp/pkg/modules"
@@ -77,7 +78,8 @@ func initLicenseValidation(ctx context.Context, config *config.Config) (*License
7778
}
7879

7980
// Use the NGManager service for license validation
80-
licenseClient, err := utils.CreateLicenseClient(
81+
licenseClient, err := license.CreateCustomLicenseClientWithContext(
82+
ctx,
8183
config,
8284
config.NgManagerBaseURL,
8385
config.BaseURL,
@@ -88,7 +90,7 @@ func initLicenseValidation(ctx context.Context, config *config.Config) (*License
8890
return licenseInfo, fmt.Errorf("failed to create license client, error: %w", err)
8991
}
9092

91-
slog.Info("Successfully created license client", "baseURL", config.NgManagerBaseURL)
93+
slog.Info("Successfully created license client")
9294

9395
// Call GetAccountLicensesWithResponse to get account licenses
9496
// Make the API call
@@ -172,7 +174,7 @@ func InitToolsets(ctx context.Context, config *config.Config) (*toolsets.Toolset
172174
enabledModules := getEnabledModules(configEnabledModules, licenseInfo)
173175
// Register toolsets for enabled modules
174176
for _, module := range enabledModules {
175-
slog.Info("registering toolsets for", "modules: ", module.ID())
177+
slog.Info("registering toolsets for", "modules", module.ID())
176178
if err := module.RegisterToolsets(); err != nil {
177179
return nil, fmt.Errorf("failed to register toolsets for module %s: %w", module.ID(), err)
178180
}

pkg/modules/utils/license.go

Lines changed: 0 additions & 27 deletions
This file was deleted.

0 commit comments

Comments
 (0)