From 0e2110f17709938f9bc4c858898f1bed777849c8 Mon Sep 17 00:00:00 2001 From: Robert Chiniquy Date: Sat, 7 Feb 2026 19:16:27 -0800 Subject: [PATCH 1/2] 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/2] 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) }