From 0e2110f17709938f9bc4c858898f1bed777849c8 Mon Sep 17 00:00:00 2001 From: Robert Chiniquy Date: Sat, 7 Feb 2026 19:16:27 -0800 Subject: [PATCH 1/5] cone install-mcp for Claude Code --- cmd/cone/install_mcp.go | 132 +++++++++++++++++++++++++++++++++++ cmd/cone/install_mcp_test.go | 59 ++++++++++++++++ cmd/cone/main.go | 1 + 3 files changed, 192 insertions(+) create mode 100644 cmd/cone/install_mcp.go create mode 100644 cmd/cone/install_mcp_test.go diff --git a/cmd/cone/install_mcp.go b/cmd/cone/install_mcp.go new file mode 100644 index 00000000..24d21018 --- /dev/null +++ b/cmd/cone/install_mcp.go @@ -0,0 +1,132 @@ +package main + +import ( + "encoding/json" + "fmt" + "os/exec" + "strings" + + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +func installMCPCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "install-mcp", + Short: "Connect Claude Code to ConductorOne's hosted MCP gateway.", + Long: `Registers ConductorOne's hosted MCP endpoint with Claude Code. + +Claude Code handles OAuth (including DCR) on first connection. +Cone just provides the endpoint URL based on your existing login. + +Requires 'cone login ' to have been run first.`, + RunE: installMCPRun, + } + + cmd.Flags().String("scope", "user", "Claude Code scope: user or project") + cmd.Flags().Bool("dry-run", false, "Print what would happen without doing it") + cmd.Flags().Bool("manual", false, "Print config snippet instead of running claude CLI") + + return cmd +} + +func installMCPRun(cmd *cobra.Command, args []string) error { + v, err := getSubViperForProfile(cmd) + if err != nil { + return err + } + + clientID := v.GetString("client-id") + if clientID == "" { + return fmt.Errorf("not authenticated. Run 'cone login ' first") + } + + // Parse tenant host from client-id. + // Format: {name}@{host}/{path} + tenantHost, err := parseTenantHost(clientID) + if err != nil { + return fmt.Errorf("could not determine tenant from client-id %q: %w", clientID, err) + } + + mcpURL := fmt.Sprintf("https://%s/api/v1alpha/mcp", tenantHost) + + profile := v.GetString("profile") + if profile == "" { + profile = "default" + } + serverName := "conductorone" + if profile != "default" { + serverName = fmt.Sprintf("conductorone-%s", profile) + } + + scope, _ := cmd.Flags().GetString("scope") + dryRun, _ := cmd.Flags().GetBool("dry-run") + manual, _ := cmd.Flags().GetBool("manual") + + if dryRun { + fmt.Printf("Would run:\n claude mcp add --transport http --scope %s %s %s\n", scope, serverName, mcpURL) + return nil + } + + if manual { + printManualMCPConfig(serverName, mcpURL) + return nil + } + + claudePath, err := exec.LookPath("claude") + if err != nil { + fmt.Printf("claude CLI not found on PATH. Falling back to manual config.\n\n") + printManualMCPConfig(serverName, mcpURL) + return nil + } + + spinner, err := pterm.DefaultSpinner.Start(fmt.Sprintf("Registering MCP server %q...", serverName)) + if err != nil { + return err + } + + out, err := exec.CommandContext(cmd.Context(), claudePath, "mcp", "add", + "--transport", "http", + "--scope", scope, + serverName, mcpURL, + ).CombinedOutput() + if err != nil { + spinner.Fail(fmt.Sprintf("Failed to register: %s", strings.TrimSpace(string(out)))) + return fmt.Errorf("claude mcp add failed: %w", err) + } + + spinner.Success(fmt.Sprintf("Registered %q (scope: %s)", serverName, scope)) + fmt.Printf("\nEndpoint: %s\n", mcpURL) + fmt.Printf("Claude Code will handle OAuth on first connection.\n") + fmt.Printf("Restart Claude Code or run /mcp to connect.\n") + + return nil +} + +// parseTenantHost extracts the host from a C1 client-id. +// Client-id format: {name}@{host}/{path} +func parseTenantHost(clientID string) (string, error) { + parts := strings.SplitN(clientID, "@", 2) + if len(parts) != 2 { + return "", fmt.Errorf("expected format {name}@{host}/{path}") + } + hostPath := parts[1] + hostParts := strings.SplitN(hostPath, "/", 2) + if len(hostParts) != 2 { + return "", fmt.Errorf("expected format {name}@{host}/{path}") + } + return hostParts[0], nil +} + +func printManualMCPConfig(serverName, mcpURL string) { + config := map[string]any{ + "type": "http", + "url": mcpURL, + } + configJSON, _ := json.MarshalIndent(map[string]any{ + serverName: config, + }, "", " ") + + fmt.Printf("Add the following to your Claude Code MCP config:\n\n") + fmt.Printf("%s\n", configJSON) +} diff --git a/cmd/cone/install_mcp_test.go b/cmd/cone/install_mcp_test.go new file mode 100644 index 00000000..c7d5347e --- /dev/null +++ b/cmd/cone/install_mcp_test.go @@ -0,0 +1,59 @@ +package main + +import ( + "testing" +) + +func TestParseTenantHost(t *testing.T) { + tests := []struct { + name string + clientID string + want string + wantErr bool + }{ + { + name: "standard client-id", + clientID: "myapp@mycompany.conductor.one/api", + want: "mycompany.conductor.one", + }, + { + name: "client-id with longer path", + clientID: "svc@staging.conductor.one/api/v1/something", + want: "staging.conductor.one", + }, + { + name: "missing @ separator", + clientID: "nohostpart", + wantErr: true, + }, + { + name: "missing / after host", + clientID: "name@hostonly", + wantErr: true, + }, + { + name: "empty string", + clientID: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseTenantHost(tt.clientID) + if tt.wantErr { + if err == nil { + t.Errorf("parseTenantHost(%q) expected error, got %q", tt.clientID, got) + } + return + } + if err != nil { + t.Errorf("parseTenantHost(%q) unexpected error: %v", tt.clientID, err) + return + } + if got != tt.want { + t.Errorf("parseTenantHost(%q) = %q, want %q", tt.clientID, got, tt.want) + } + }) + } +} diff --git a/cmd/cone/main.go b/cmd/cone/main.go index dc264b2e..834c0ab6 100644 --- a/cmd/cone/main.go +++ b/cmd/cone/main.go @@ -80,6 +80,7 @@ func runCli(ctx context.Context) int { cliCmd.AddCommand(hasCmd()) cliCmd.AddCommand(tokenCmd()) cliCmd.AddCommand(decryptCredentialCmd()) + cliCmd.AddCommand(installMCPCmd()) err = cliCmd.ExecuteContext(ctx) if err != nil { From 5d39f5f79605342b40eb3ca38720f98e98b23c75 Mon Sep 17 00:00:00 2001 From: Robert Chiniquy Date: Sat, 7 Feb 2026 19:36:20 -0800 Subject: [PATCH 2/5] Lintfix but also cone install-mcp for Claude Code --- .golangci.yml | 16 ++++++---------- cmd/cone/install_mcp.go | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 4ab47e29..5c633be8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,3 +1,5 @@ +version: "2" + linters-settings: exhaustive: default-signifies-exhaustive: true @@ -47,22 +49,14 @@ linters-settings: - name: var-naming arguments: [["ID", "URL", "HTTP", "API"], []] - tenv: - all: true - - varcheck: - exported-fields: false # this appears to improperly detect exported variables as unused when they are used from a package with the same name - linters: disable-all: true enable: - errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases - - gosimple # Linter for Go source code that specializes in simplifying a code - govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string - ineffassign # Detects when assignments to existing variables are not used - staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks - - typecheck # Like the front-end of a Go compiler, parses and type-checks Go code - unused # Checks Go code for unused constants, variables, functions and types - asasalint # Check for pass []any as any in variadic func(...any) - asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers @@ -76,7 +70,6 @@ linters: - goconst # Finds repeated strings that could be replaced by a constant - gocritic # Provides diagnostics that check for bugs, performance and style issues. - godot # Check if comments end in a period - - goimports # In addition to fixing imports, goimports also formats your code in the same style as gofmt. - gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. - goprintffuncname # Checks that printf-like functions are named with f at the end - gosec # Inspects source code for security problems @@ -88,12 +81,15 @@ linters: - nosprintfhostport # Checks for misuse of Sprintf to construct a host with port in a URL. - predeclared # find code that shadows one of Go's predeclared identifiers - revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. - - tenv # tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 - tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes - unconvert # Remove unnecessary type conversions - usestdlibvars # detect the possibility to use variables/constants from the Go standard library - whitespace # Tool for detection of leading and trailing whitespace +formatters: + enable: + - goimports # In addition to fixing imports, goimports also formats your code in the same style as gofmt. + issues: max-same-issues: 50 diff --git a/cmd/cone/install_mcp.go b/cmd/cone/install_mcp.go index 24d21018..4984ced6 100644 --- a/cmd/cone/install_mcp.go +++ b/cmd/cone/install_mcp.go @@ -30,7 +30,7 @@ Requires 'cone login ' to have been run first.`, return cmd } -func installMCPRun(cmd *cobra.Command, args []string) error { +func installMCPRun(cmd *cobra.Command, _ []string) error { v, err := getSubViperForProfile(cmd) if err != nil { return err @@ -64,7 +64,7 @@ func installMCPRun(cmd *cobra.Command, args []string) error { manual, _ := cmd.Flags().GetBool("manual") if dryRun { - fmt.Printf("Would run:\n claude mcp add --transport http --scope %s %s %s\n", scope, serverName, mcpURL) + pterm.Printf("Would run:\n claude mcp add --transport http --scope %s %s %s\n", scope, serverName, mcpURL) return nil } @@ -73,9 +73,9 @@ func installMCPRun(cmd *cobra.Command, args []string) error { return nil } - claudePath, err := exec.LookPath("claude") - if err != nil { - fmt.Printf("claude CLI not found on PATH. Falling back to manual config.\n\n") + claudePath, _ := exec.LookPath("claude") + if claudePath == "" { + pterm.Printf("claude CLI not found on PATH. Falling back to manual config.\n\n") printManualMCPConfig(serverName, mcpURL) return nil } @@ -85,7 +85,7 @@ func installMCPRun(cmd *cobra.Command, args []string) error { return err } - out, err := exec.CommandContext(cmd.Context(), claudePath, "mcp", "add", + out, err := exec.CommandContext(cmd.Context(), claudePath, "mcp", "add", //nolint:gosec // args are from config, not user input "--transport", "http", "--scope", scope, serverName, mcpURL, @@ -96,15 +96,15 @@ func installMCPRun(cmd *cobra.Command, args []string) error { } spinner.Success(fmt.Sprintf("Registered %q (scope: %s)", serverName, scope)) - fmt.Printf("\nEndpoint: %s\n", mcpURL) - fmt.Printf("Claude Code will handle OAuth on first connection.\n") - fmt.Printf("Restart Claude Code or run /mcp to connect.\n") + pterm.Printf("\nEndpoint: %s\n", mcpURL) + pterm.Printf("Claude Code will handle OAuth on first connection.\n") + pterm.Printf("Restart Claude Code or run /mcp to connect.\n") return nil } // parseTenantHost extracts the host from a C1 client-id. -// Client-id format: {name}@{host}/{path} +// Client-id format: {name}@{host}/{path}. func parseTenantHost(clientID string) (string, error) { parts := strings.SplitN(clientID, "@", 2) if len(parts) != 2 { @@ -127,6 +127,6 @@ func printManualMCPConfig(serverName, mcpURL string) { serverName: config, }, "", " ") - fmt.Printf("Add the following to your Claude Code MCP config:\n\n") - fmt.Printf("%s\n", configJSON) + pterm.Printf("Add the following to your Claude Code MCP config:\n\n") + pterm.Printf("%s\n", configJSON) } From c742a37d5157a6a403053e1e4e90239d7310b3b1 Mon Sep 17 00:00:00 2001 From: Robert Chiniquy Date: Tue, 31 Mar 2026 11:41:24 -0700 Subject: [PATCH 3/5] Update CI to golangci-lint v2 --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3d89528a..66318c52 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,9 +11,9 @@ jobs: - name: Checkout code uses: actions/checkout@v3 - name: Run linters - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v7 with: - version: latest + version: v2.1 args: --timeout=3m go-test: strategy: From d423849158e0ea99929a1df3bd3ee7f98c64d36f Mon Sep 17 00:00:00 2001 From: Robert Chiniquy Date: Tue, 31 Mar 2026 11:42:54 -0700 Subject: [PATCH 4/5] Migrate golangci-lint config to v2 schema --- .golangci.yml | 160 ++++++++++++++++++++++---------------------------- 1 file changed, 71 insertions(+), 89 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 5c633be8..1bf13540 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,99 +1,81 @@ version: "2" -linters-settings: - exhaustive: - default-signifies-exhaustive: true - - gocritic: - # The list of supported checkers can be find in https://go-critic.github.io/overview. - settings: - underef: - # Whether to skip (*x).method() calls where x is a pointer receiver. - skipRecvDeref: false - - govet: - enable-all: true - disable: - - fieldalignment # too strict - - shadow # complains too much about shadowing errors. All research points to this being fine. - - nakedret: - max-func-lines: 0 - - nolintlint: - allow-no-explanation: [ forbidigo, tracecheck, gomnd, gochecknoinits, makezero ] - require-explanation: true - require-specific: true - - revive: - ignore-generated-header: true - severity: error - rules: - - name: atomic - - name: line-length-limit - arguments: [ 200 ] - # These are functions that we use without checking the errors often. Most of these can't return an error even - # though they implement an interface that can. - - name: unhandled-error - arguments: - - fmt.Printf - - fmt.Println - - fmt.Fprintf - - fmt.Fprintln - - os.Stderr.Sync - - sb.WriteString - - buf.WriteString - - hasher.Write - - os.Setenv - - os.RemoveAll - - name: var-naming - arguments: [["ID", "URL", "HTTP", "API"], []] - - linters: - disable-all: true enable: - - errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases - - govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string - - ineffassign # Detects when assignments to existing variables are not used - - staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks - - unused # Checks Go code for unused constants, variables, functions and types - - asasalint # Check for pass []any as any in variadic func(...any) - - asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers - - bidichk # Checks for dangerous unicode character sequences - - bodyclose # checks whether HTTP response body is closed successfully - - durationcheck # check for two durations multiplied together - - errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. - - exhaustive # check exhaustiveness of enum switch statements - - forbidigo # Forbids identifiers - - gochecknoinits # Checks that no init functions are present in Go code - - goconst # Finds repeated strings that could be replaced by a constant - - gocritic # Provides diagnostics that check for bugs, performance and style issues. - - godot # Check if comments end in a period - - gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. - - goprintffuncname # Checks that printf-like functions are named with f at the end - - gosec # Inspects source code for security problems - - nakedret # Finds naked returns in functions greater than a specified function length - - nilerr # Finds the code that returns nil even if it checks that the error is not nil. - - noctx # noctx finds sending http request without context.Context - - nolintlint # Reports ill-formed or insufficient nolint directives - - nonamedreturns # Reports all named returns - - nosprintfhostport # Checks for misuse of Sprintf to construct a host with port in a URL. - - predeclared # find code that shadows one of Go's predeclared identifiers - - revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. - - tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes - - unconvert # Remove unnecessary type conversions - - usestdlibvars # detect the possibility to use variables/constants from the Go standard library - - whitespace # Tool for detection of leading and trailing whitespace + - errcheck + - govet + - ineffassign + - staticcheck + - unused + - asasalint + - asciicheck + - bidichk + - bodyclose + - durationcheck + - errorlint + - exhaustive + - forbidigo + - gochecknoinits + - goconst + - gocritic + - godot + - gomoddirectives + - goprintffuncname + - gosec + - nakedret + - nilerr + - noctx + - nolintlint + - nonamedreturns + - nosprintfhostport + - predeclared + - revive + - tparallel + - unconvert + - usestdlibvars + - whitespace + settings: + exhaustive: + default-signifies-exhaustive: true + gocritic: + settings: + underef: + skipRecvDeref: false + govet: + enable-all: true + disable: + - fieldalignment + - shadow + nakedret: + max-func-lines: 0 + nolintlint: + allow-no-explanation: [forbidigo, tracecheck, gomnd, gochecknoinits, makezero] + require-explanation: true + require-specific: true + revive: + severity: error + rules: + - name: atomic + - name: line-length-limit + arguments: [200] + - name: unhandled-error + arguments: + - fmt.Printf + - fmt.Println + - fmt.Fprintf + - fmt.Fprintln + - os.Stderr.Sync + - sb.WriteString + - buf.WriteString + - hasher.Write + - os.Setenv + - os.RemoveAll + - name: var-naming + arguments: [["ID", "URL", "HTTP", "API"], []] formatters: enable: - - goimports # In addition to fixing imports, goimports also formats your code in the same style as gofmt. + - goimports issues: max-same-issues: 50 - - exclude-rules: - # Don't require TODO comments to end in a period - - source: "(TODO)" - linters: [ godot ] From 356fe9ed366e89b5826a547c37cd456ef29337b4 Mon Sep 17 00:00:00 2001 From: Robert Chiniquy Date: Tue, 31 Mar 2026 12:52:08 -0700 Subject: [PATCH 5/5] Fix lint errors uncovered by golangci-lint v2 migration --- .golangci.yml | 2 ++ cmd/cone/aws.go | 8 ++++---- cmd/cone/virtual_entitlements.go | 2 +- pkg/client/token_exchange.go | 2 +- pkg/client/token_source.go | 2 +- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 1bf13540..71294530 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -37,6 +37,8 @@ linters: settings: exhaustive: default-signifies-exhaustive: true + goconst: + min-occurrences: 5 gocritic: settings: underef: diff --git a/cmd/cone/aws.go b/cmd/cone/aws.go index f451ffe1..496fc935 100644 --- a/cmd/cone/aws.go +++ b/cmd/cone/aws.go @@ -221,7 +221,7 @@ func createAWSProfile(entitlement *shared.AppEntitlement, resource *shared.AppRe } configPath := filepath.Join(awsConfigDir, "config") - configContent, err := os.ReadFile(configPath) + configContent, err := os.ReadFile(configPath) //nolint:gosec // path from known config dir if err != nil && !os.IsNotExist(err) { return "", fmt.Errorf("failed to read AWS config: %w", err) } @@ -390,7 +390,7 @@ func awsCredentialsRun(cmd *cobra.Command, args []string) error { awsConfigDir := filepath.Join(os.Getenv("HOME"), ".aws") configPath := filepath.Join(awsConfigDir, "config") - configContent, err := os.ReadFile(configPath) + configContent, err := os.ReadFile(configPath) //nolint:gosec // path from known config dir if err != nil { return fmt.Errorf("failed to read AWS config: %w", err) } @@ -419,7 +419,7 @@ func awsCredentialsRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to marshal credentials: %w", err) } - fmt.Fprintln(os.Stdout, string(jsonOutput)) + fmt.Fprintln(os.Stdout, string(jsonOutput)) //nolint:errcheck // writing to stdout return nil } @@ -469,7 +469,7 @@ func getSSOToken(ssoStartURL string) (string, error) { if file.IsDir() || !strings.HasSuffix(file.Name(), ".json") { continue } - content, err := os.ReadFile(filepath.Join(cacheDir, file.Name())) + content, err := os.ReadFile(filepath.Join(cacheDir, file.Name())) //nolint:gosec // path from known cache dir if err != nil { continue } diff --git a/cmd/cone/virtual_entitlements.go b/cmd/cone/virtual_entitlements.go index b009343d..89f3ab5e 100644 --- a/cmd/cone/virtual_entitlements.go +++ b/cmd/cone/virtual_entitlements.go @@ -109,7 +109,7 @@ func virtualEntitlementsCreateRun(cmd *cobra.Command, args []string) error { var resources []virtualResourceYAML if fromFile != "" { - data, err := os.ReadFile(fromFile) + data, err := os.ReadFile(fromFile) //nolint:gosec // path from CLI flag if err != nil { return fmt.Errorf("reading file %s: %w", fromFile, err) } diff --git a/pkg/client/token_exchange.go b/pkg/client/token_exchange.go index b611ad3e..ea32d706 100644 --- a/pkg/client/token_exchange.go +++ b/pkg/client/token_exchange.go @@ -54,7 +54,7 @@ func (t *tokenExchangeSource) Token() (*oauth2.Token, error) { if err != nil { return nil, err } - defer resp.Body.Close() + defer resp.Body.Close() //nolint:errcheck // best-effort close if resp.StatusCode != http.StatusOK { return nil, status.Errorf(codes.Unauthenticated, "token exchange failed: %s", resp.Status) diff --git a/pkg/client/token_source.go b/pkg/client/token_source.go index 7c166897..e85b25d2 100644 --- a/pkg/client/token_source.go +++ b/pkg/client/token_source.go @@ -152,7 +152,7 @@ func (c *c1TokenSource) Token() (*oauth2.Token, error) { if err != nil { return nil, err } - defer resp.Body.Close() + defer resp.Body.Close() //nolint:errcheck // best-effort close if resp.StatusCode != http.StatusOK { return nil, status.Errorf(codes.Unauthenticated, "failed to get token: %s", resp.Status)