Skip to content

Commit 95ac8b7

Browse files
committed
derive account ID from api key, rename org/project so it's clear they are default values
1 parent dddfd61 commit 95ac8b7

File tree

4 files changed

+80
-52
lines changed

4 files changed

+80
-52
lines changed

README.md

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ go build -o cmd/harness-mcp-server/harness-mcp-server ./cmd/harness-mcp-server
5050

5151
3. Run the server:
5252
```bash
53-
HARNESS_API_KEY=your_api_key HARNESS_ACCOUNT_ID=your_account_id HARNESS_ORG_ID=your_org_id HARNESS_PROJECT_ID=your_project_id ./cmd/harness-mcp-server/harness-mcp-server stdio
53+
HARNESS_API_KEY=your_api_key HARNESS_DEFAULT_ORG_ID=your_org_id HARNESS_DEFAULT_PROJECT_ID=your_project_id ./cmd/harness-mcp-server/harness-mcp-server stdio
5454
```
5555

5656
### Claude Desktop Configuration
@@ -69,9 +69,8 @@ On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
6969
"args": ["stdio"],
7070
"env": {
7171
"HARNESS_API_KEY": "<YOUR_API_KEY>",
72-
"HARNESS_ACCOUNT_ID": "<YOUR_ACCOUNT_ID>",
73-
"HARNESS_ORG_ID": "<YOUR_ORG_ID>",
74-
"HARNESS_PROJECT_ID": "<YOUR_PROJECT_ID>"
72+
"HARNESS_DEFAULT_ORG_ID": "<YOUR_ORG_ID>",
73+
"HARNESS_DEFAULT_PROJECT_ID": "<YOUR_PROJECT_ID>"
7574
}
7675
}
7776
}
@@ -99,10 +98,9 @@ To use the Harness MCP Server with Windsurf:
9998
"args": ["stdio"],
10099
"env": {
101100
"HARNESS_API_KEY": "<YOUR_API_KEY>",
102-
"HARNESS_ACCOUNT_ID": "<YOUR_ACCOUNT_ID>",
103-
"HARNESS_ORG_ID": "<YOUR_ORG_ID>",
104-
"HARNESS_PROJECT_ID": "<YOUR_PROJECT_ID>",
105-
"HARNESS_BASE_URL": "<YOUR_BASE_URL>",
101+
"HARNESS_DEFAULT_ORG_ID": "<YOUR_ORG_ID>",
102+
"HARNESS_DEFAULT_PROJECT_ID": "<YOUR_PROJECT_ID>",
103+
"HARNESS_BASE_URL": "<YOUR_BASE_URL>"
106104
}
107105
}
108106
}
@@ -127,10 +125,9 @@ The Harness MCP Server supports the following command line arguments:
127125

128126
Environment variables are prefixed with `HARNESS_`:
129127

130-
- `HARNESS_API_KEY`: Harness API key (required)
131-
- `HARNESS_ACCOUNT_ID`: Harness account ID (required)
132-
- `HARNESS_ORG_ID`: Harness organization ID (optional, but required for some operations)
133-
- `HARNESS_PROJECT_ID`: Harness project ID (optional, but required for some operations)
128+
- `HARNESS_API_KEY`: Harness API key (required) - Account ID is automatically extracted from the API key
129+
- `HARNESS_DEFAULT_ORG_ID`: Default Harness organization ID (optional, if not specified it would need to be passed in the request if it's required for that operation)
130+
- `HARNESS_DEFAULT_PROJECT_ID`: Default Harness project ID (optional, if not specified it would need to be passed in the request if it's required for that operation)
134131
- `HARNESS_TOOLSETS`: Comma-separated list of toolsets to enable (default: "all")
135132
- `HARNESS_READ_ONLY`: Set to "true" to run in read-only mode
136133
- `HARNESS_LOG_FILE`: Path to log file
Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
package config
22

33
type Config struct {
4-
Version string
5-
BaseURL string
6-
AccountID string
7-
OrgID string
8-
ProjectID string
9-
APIKey string
10-
ReadOnly bool
11-
Toolsets []string
12-
LogFilePath string
13-
Debug bool
4+
Version string
5+
BaseURL string
6+
AccountID string
7+
DefaultOrgID string
8+
DefaultProjectID string
9+
APIKey string
10+
ReadOnly bool
11+
Toolsets []string
12+
LogFilePath string
13+
Debug bool
1414
}

cmd/harness-mcp-server/main.go

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"log/slog"
99
"os"
1010
"os/signal"
11+
"strings"
1112
"syscall"
1213

1314
"github.com/harness/harness-mcp/client"
@@ -23,6 +24,16 @@ var version = "0.1.0"
2324
var commit = "dev"
2425
var date = "unknown"
2526

27+
// extractAccountIDFromAPIKey extracts the account ID from a Harness API key
28+
// API key format: pat.ACCOUNT_ID.TOKEN_ID.<>
29+
func extractAccountIDFromAPIKey(apiKey string) (string, error) {
30+
parts := strings.Split(apiKey, ".")
31+
if len(parts) < 2 {
32+
return "", fmt.Errorf("invalid API key format")
33+
}
34+
return parts[1], nil
35+
}
36+
2637
var (
2738
rootCmd = &cobra.Command{
2839
Use: "harness-mcp-server",
@@ -36,28 +47,34 @@ var (
3647
Short: "Start stdio server",
3748
Long: `Start a server that communicates via standard input/output streams using JSON-RPC messages.`,
3849
RunE: func(_ *cobra.Command, _ []string) error {
39-
token := viper.GetString("api_key")
40-
if token == "" {
50+
apiKey := viper.GetString("api_key")
51+
if apiKey == "" {
4152
return fmt.Errorf("API key not provided")
4253
}
4354

55+
// Extract account ID from API key
56+
accountID, err := extractAccountIDFromAPIKey(apiKey)
57+
if err != nil {
58+
return fmt.Errorf("failed to extract account ID from API key: %w", err)
59+
}
60+
4461
var toolsets []string
45-
err := viper.UnmarshalKey("toolsets", &toolsets)
62+
err = viper.UnmarshalKey("toolsets", &toolsets)
4663
if err != nil {
4764
return fmt.Errorf("Failed to unmarshal toolsets: %w", err)
4865
}
4966

5067
cfg := config.Config{
51-
Version: version,
52-
BaseURL: viper.GetString("base_url"),
53-
AccountID: viper.GetString("account_id"),
54-
OrgID: viper.GetString("org_id"),
55-
ProjectID: viper.GetString("project_id"),
56-
APIKey: viper.GetString("api_key"),
57-
ReadOnly: viper.GetBool("read_only"),
58-
Toolsets: toolsets,
59-
LogFilePath: viper.GetString("log_file"),
60-
Debug: viper.GetBool("debug"),
68+
Version: version,
69+
BaseURL: viper.GetString("base_url"),
70+
AccountID: accountID,
71+
DefaultOrgID: viper.GetString("default_org_id"),
72+
DefaultProjectID: viper.GetString("default_project_id"),
73+
APIKey: apiKey,
74+
ReadOnly: viper.GetBool("read_only"),
75+
Toolsets: toolsets,
76+
LogFilePath: viper.GetString("log_file"),
77+
Debug: viper.GetBool("debug"),
6178
}
6279

6380
if err := runStdioServer(cfg); err != nil {
@@ -80,9 +97,8 @@ func init() {
8097
rootCmd.PersistentFlags().Bool("debug", false, "Enable debug logging")
8198
rootCmd.PersistentFlags().String("base-url", "https://app.harness.io", "Base URL for Harness")
8299
rootCmd.PersistentFlags().String("api-key", "", "API key for authentication")
83-
rootCmd.PersistentFlags().String("account-id", "", "Account ID to use")
84-
rootCmd.PersistentFlags().String("org-id", "", "(Optional) org ID to use")
85-
rootCmd.PersistentFlags().String("project-id", "", "(Optional) project ID to use")
100+
rootCmd.PersistentFlags().String("default-org-id", "", "Default org ID to use. If not specified, it would need to be passed in the query (if required)")
101+
rootCmd.PersistentFlags().String("default-project-id", "", "Default project ID to use. If not specified, it would need to be passed in the query (if required)")
86102

87103
// Bind flags to viper
88104
_ = viper.BindPFlag("toolsets", rootCmd.PersistentFlags().Lookup("toolsets"))
@@ -91,9 +107,8 @@ func init() {
91107
_ = viper.BindPFlag("debug", rootCmd.PersistentFlags().Lookup("debug"))
92108
_ = viper.BindPFlag("base_url", rootCmd.PersistentFlags().Lookup("base-url"))
93109
_ = viper.BindPFlag("api_key", rootCmd.PersistentFlags().Lookup("api-key"))
94-
_ = viper.BindPFlag("account_id", rootCmd.PersistentFlags().Lookup("account-id"))
95-
_ = viper.BindPFlag("org_id", rootCmd.PersistentFlags().Lookup("org-id"))
96-
_ = viper.BindPFlag("project_id", rootCmd.PersistentFlags().Lookup("project-id"))
110+
_ = viper.BindPFlag("default_org_id", rootCmd.PersistentFlags().Lookup("default-org-id"))
111+
_ = viper.BindPFlag("default_project_id", rootCmd.PersistentFlags().Lookup("default-project-id"))
97112

98113
// Add subcommands
99114
rootCmd.AddCommand(stdioCmd)

pkg/harness/scope.go

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,37 @@ import (
88
"github.com/mark3labs/mcp-go/mcp"
99
)
1010

11-
// WithScope adds org_id and project_id as optional parameters if they are not already defined in the
12-
// config.
11+
// NoopPropertyOption is a property option that does nothing.
12+
func NoopPropertyOption() mcp.PropertyOption {
13+
return func(schema map[string]interface{}) {}
14+
}
15+
16+
// WithScope adds org_id and project_id as optional/required parameters, based on
17+
// what is set in the env variables.
1318
func WithScope(config *config.Config, required bool) mcp.ToolOption {
14-
var opt mcp.PropertyOption
19+
opt := NoopPropertyOption()
1520
if required {
1621
opt = mcp.Required()
1722
}
1823
return func(tool *mcp.Tool) {
24+
defaultProjectOpt := NoopPropertyOption()
25+
defaultOrgOpt := NoopPropertyOption()
26+
if config.DefaultOrgID != "" {
27+
defaultOrgOpt = mcp.DefaultString(config.DefaultOrgID)
28+
}
29+
if config.DefaultProjectID != "" {
30+
defaultProjectOpt = mcp.DefaultString(config.DefaultProjectID)
31+
}
1932
mcp.WithString("org_id",
2033
mcp.Description("The ID of the organization."),
21-
mcp.DefaultString(config.OrgID),
34+
defaultOrgOpt,
2235
opt,
23-
)
36+
)(tool)
2437
mcp.WithString("project_id",
2538
mcp.Description("The ID of the project."),
26-
mcp.DefaultString(config.ProjectID),
39+
defaultProjectOpt,
2740
opt,
28-
)
41+
)(tool)
2942
}
3043
}
3144

@@ -41,8 +54,8 @@ func fetchScope(config *config.Config, request mcp.CallToolRequest, required boo
4154

4255
scope := dto.Scope{
4356
AccountID: config.AccountID,
44-
OrgID: config.OrgID,
45-
ProjectID: config.ProjectID,
57+
OrgID: config.DefaultOrgID,
58+
ProjectID: config.DefaultProjectID,
4659
}
4760

4861
// try to fetch it from the MCP request
@@ -58,8 +71,11 @@ func fetchScope(config *config.Config, request mcp.CallToolRequest, required boo
5871
// org ID and project ID may or may not be required for APIs. If they are required, we return an error
5972
// if not present.
6073
if required {
61-
if scope.OrgID == "" || scope.ProjectID == "" {
62-
return scope, fmt.Errorf("org ID and project ID are required")
74+
if scope.OrgID == "" {
75+
return scope, fmt.Errorf("org ID is required")
76+
}
77+
if scope.ProjectID == "" {
78+
return scope, fmt.Errorf("project ID is required")
6379
}
6480
}
6581

0 commit comments

Comments
 (0)