Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 6 additions & 10 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
version: "2"

linters-settings:
exhaustive:
default-signifies-exhaustive: true
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand Down
132 changes: 132 additions & 0 deletions cmd/cone/install_mcp.go
Original file line number Diff line number Diff line change
@@ -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 <tenant>' 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, _ []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 <tenant>' 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 {
pterm.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, _ := 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
}

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", //nolint:gosec // args are from config, not user input
"--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))
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}.
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,
}, "", " ")

pterm.Printf("Add the following to your Claude Code MCP config:\n\n")
pterm.Printf("%s\n", configJSON)
}
59 changes: 59 additions & 0 deletions cmd/cone/install_mcp_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
1 change: 1 addition & 0 deletions cmd/cone/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading