Skip to content

Commit abe2f87

Browse files
cone install-mcp (#133)
* cone install-mcp for Claude Code * Lintfix but also cone install-mcp for Claude Code * Update CI to golangci-lint v2 * Migrate golangci-lint config to v2 schema * Fix lint errors uncovered by golangci-lint v2 migration
1 parent c2f4fa2 commit abe2f87

File tree

9 files changed

+278
-106
lines changed

9 files changed

+278
-106
lines changed

.github/workflows/ci.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ jobs:
1111
- name: Checkout code
1212
uses: actions/checkout@v3
1313
- name: Run linters
14-
uses: golangci/golangci-lint-action@v3
14+
uses: golangci/golangci-lint-action@v7
1515
with:
16-
version: latest
16+
version: v2.1
1717
args: --timeout=3m
1818
go-test:
1919
strategy:

.golangci.yml

Lines changed: 77 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,83 @@
1-
linters-settings:
2-
exhaustive:
3-
default-signifies-exhaustive: true
4-
5-
gocritic:
6-
# The list of supported checkers can be find in https://go-critic.github.io/overview.
7-
settings:
8-
underef:
9-
# Whether to skip (*x).method() calls where x is a pointer receiver.
10-
skipRecvDeref: false
11-
12-
govet:
13-
enable-all: true
14-
disable:
15-
- fieldalignment # too strict
16-
- shadow # complains too much about shadowing errors. All research points to this being fine.
17-
18-
nakedret:
19-
max-func-lines: 0
20-
21-
nolintlint:
22-
allow-no-explanation: [ forbidigo, tracecheck, gomnd, gochecknoinits, makezero ]
23-
require-explanation: true
24-
require-specific: true
25-
26-
revive:
27-
ignore-generated-header: true
28-
severity: error
29-
rules:
30-
- name: atomic
31-
- name: line-length-limit
32-
arguments: [ 200 ]
33-
# These are functions that we use without checking the errors often. Most of these can't return an error even
34-
# though they implement an interface that can.
35-
- name: unhandled-error
36-
arguments:
37-
- fmt.Printf
38-
- fmt.Println
39-
- fmt.Fprintf
40-
- fmt.Fprintln
41-
- os.Stderr.Sync
42-
- sb.WriteString
43-
- buf.WriteString
44-
- hasher.Write
45-
- os.Setenv
46-
- os.RemoveAll
47-
- name: var-naming
48-
arguments: [["ID", "URL", "HTTP", "API"], []]
49-
50-
tenv:
51-
all: true
52-
53-
varcheck:
54-
exported-fields: false # this appears to improperly detect exported variables as unused when they are used from a package with the same name
55-
1+
version: "2"
562

573
linters:
58-
disable-all: true
594
enable:
60-
- errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases
61-
- gosimple # Linter for Go source code that specializes in simplifying a code
62-
- govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string
63-
- ineffassign # Detects when assignments to existing variables are not used
64-
- staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks
65-
- typecheck # Like the front-end of a Go compiler, parses and type-checks Go code
66-
- unused # Checks Go code for unused constants, variables, functions and types
67-
- asasalint # Check for pass []any as any in variadic func(...any)
68-
- asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers
69-
- bidichk # Checks for dangerous unicode character sequences
70-
- bodyclose # checks whether HTTP response body is closed successfully
71-
- durationcheck # check for two durations multiplied together
72-
- 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.
73-
- exhaustive # check exhaustiveness of enum switch statements
74-
- forbidigo # Forbids identifiers
75-
- gochecknoinits # Checks that no init functions are present in Go code
76-
- goconst # Finds repeated strings that could be replaced by a constant
77-
- gocritic # Provides diagnostics that check for bugs, performance and style issues.
78-
- godot # Check if comments end in a period
79-
- goimports # In addition to fixing imports, goimports also formats your code in the same style as gofmt.
80-
- gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod.
81-
- goprintffuncname # Checks that printf-like functions are named with f at the end
82-
- gosec # Inspects source code for security problems
83-
- nakedret # Finds naked returns in functions greater than a specified function length
84-
- nilerr # Finds the code that returns nil even if it checks that the error is not nil.
85-
- noctx # noctx finds sending http request without context.Context
86-
- nolintlint # Reports ill-formed or insufficient nolint directives
87-
- nonamedreturns # Reports all named returns
88-
- nosprintfhostport # Checks for misuse of Sprintf to construct a host with port in a URL.
89-
- predeclared # find code that shadows one of Go's predeclared identifiers
90-
- revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint.
91-
- tenv # tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17
92-
- tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes
93-
- unconvert # Remove unnecessary type conversions
94-
- usestdlibvars # detect the possibility to use variables/constants from the Go standard library
95-
- whitespace # Tool for detection of leading and trailing whitespace
5+
- errcheck
6+
- govet
7+
- ineffassign
8+
- staticcheck
9+
- unused
10+
- asasalint
11+
- asciicheck
12+
- bidichk
13+
- bodyclose
14+
- durationcheck
15+
- errorlint
16+
- exhaustive
17+
- forbidigo
18+
- gochecknoinits
19+
- goconst
20+
- gocritic
21+
- godot
22+
- gomoddirectives
23+
- goprintffuncname
24+
- gosec
25+
- nakedret
26+
- nilerr
27+
- noctx
28+
- nolintlint
29+
- nonamedreturns
30+
- nosprintfhostport
31+
- predeclared
32+
- revive
33+
- tparallel
34+
- unconvert
35+
- usestdlibvars
36+
- whitespace
37+
settings:
38+
exhaustive:
39+
default-signifies-exhaustive: true
40+
goconst:
41+
min-occurrences: 5
42+
gocritic:
43+
settings:
44+
underef:
45+
skipRecvDeref: false
46+
govet:
47+
enable-all: true
48+
disable:
49+
- fieldalignment
50+
- shadow
51+
nakedret:
52+
max-func-lines: 0
53+
nolintlint:
54+
allow-no-explanation: [forbidigo, tracecheck, gomnd, gochecknoinits, makezero]
55+
require-explanation: true
56+
require-specific: true
57+
revive:
58+
severity: error
59+
rules:
60+
- name: atomic
61+
- name: line-length-limit
62+
arguments: [200]
63+
- name: unhandled-error
64+
arguments:
65+
- fmt.Printf
66+
- fmt.Println
67+
- fmt.Fprintf
68+
- fmt.Fprintln
69+
- os.Stderr.Sync
70+
- sb.WriteString
71+
- buf.WriteString
72+
- hasher.Write
73+
- os.Setenv
74+
- os.RemoveAll
75+
- name: var-naming
76+
arguments: [["ID", "URL", "HTTP", "API"], []]
77+
78+
formatters:
79+
enable:
80+
- goimports
9681

9782
issues:
9883
max-same-issues: 50
99-
100-
exclude-rules:
101-
# Don't require TODO comments to end in a period
102-
- source: "(TODO)"
103-
linters: [ godot ]

cmd/cone/aws.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ func createAWSProfile(entitlement *shared.AppEntitlement, resource *shared.AppRe
221221
}
222222

223223
configPath := filepath.Join(awsConfigDir, "config")
224-
configContent, err := os.ReadFile(configPath)
224+
configContent, err := os.ReadFile(configPath) //nolint:gosec // path from known config dir
225225
if err != nil && !os.IsNotExist(err) {
226226
return "", fmt.Errorf("failed to read AWS config: %w", err)
227227
}
@@ -390,7 +390,7 @@ func awsCredentialsRun(cmd *cobra.Command, args []string) error {
390390
awsConfigDir := filepath.Join(os.Getenv("HOME"), ".aws")
391391
configPath := filepath.Join(awsConfigDir, "config")
392392

393-
configContent, err := os.ReadFile(configPath)
393+
configContent, err := os.ReadFile(configPath) //nolint:gosec // path from known config dir
394394
if err != nil {
395395
return fmt.Errorf("failed to read AWS config: %w", err)
396396
}
@@ -419,7 +419,7 @@ func awsCredentialsRun(cmd *cobra.Command, args []string) error {
419419
return fmt.Errorf("failed to marshal credentials: %w", err)
420420
}
421421

422-
fmt.Fprintln(os.Stdout, string(jsonOutput))
422+
fmt.Fprintln(os.Stdout, string(jsonOutput)) //nolint:errcheck // writing to stdout
423423
return nil
424424
}
425425

@@ -469,7 +469,7 @@ func getSSOToken(ssoStartURL string) (string, error) {
469469
if file.IsDir() || !strings.HasSuffix(file.Name(), ".json") {
470470
continue
471471
}
472-
content, err := os.ReadFile(filepath.Join(cacheDir, file.Name()))
472+
content, err := os.ReadFile(filepath.Join(cacheDir, file.Name())) //nolint:gosec // path from known cache dir
473473
if err != nil {
474474
continue
475475
}

cmd/cone/install_mcp.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os/exec"
7+
"strings"
8+
9+
"github.com/pterm/pterm"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
func installMCPCmd() *cobra.Command {
14+
cmd := &cobra.Command{
15+
Use: "install-mcp",
16+
Short: "Connect Claude Code to ConductorOne's hosted MCP gateway.",
17+
Long: `Registers ConductorOne's hosted MCP endpoint with Claude Code.
18+
19+
Claude Code handles OAuth (including DCR) on first connection.
20+
Cone just provides the endpoint URL based on your existing login.
21+
22+
Requires 'cone login <tenant>' to have been run first.`,
23+
RunE: installMCPRun,
24+
}
25+
26+
cmd.Flags().String("scope", "user", "Claude Code scope: user or project")
27+
cmd.Flags().Bool("dry-run", false, "Print what would happen without doing it")
28+
cmd.Flags().Bool("manual", false, "Print config snippet instead of running claude CLI")
29+
30+
return cmd
31+
}
32+
33+
func installMCPRun(cmd *cobra.Command, _ []string) error {
34+
v, err := getSubViperForProfile(cmd)
35+
if err != nil {
36+
return err
37+
}
38+
39+
clientID := v.GetString("client-id")
40+
if clientID == "" {
41+
return fmt.Errorf("not authenticated. Run 'cone login <tenant>' first")
42+
}
43+
44+
// Parse tenant host from client-id.
45+
// Format: {name}@{host}/{path}
46+
tenantHost, err := parseTenantHost(clientID)
47+
if err != nil {
48+
return fmt.Errorf("could not determine tenant from client-id %q: %w", clientID, err)
49+
}
50+
51+
mcpURL := fmt.Sprintf("https://%s/api/v1alpha/mcp", tenantHost)
52+
53+
profile := v.GetString("profile")
54+
if profile == "" {
55+
profile = "default"
56+
}
57+
serverName := "conductorone"
58+
if profile != "default" {
59+
serverName = fmt.Sprintf("conductorone-%s", profile)
60+
}
61+
62+
scope, _ := cmd.Flags().GetString("scope")
63+
dryRun, _ := cmd.Flags().GetBool("dry-run")
64+
manual, _ := cmd.Flags().GetBool("manual")
65+
66+
if dryRun {
67+
pterm.Printf("Would run:\n claude mcp add --transport http --scope %s %s %s\n", scope, serverName, mcpURL)
68+
return nil
69+
}
70+
71+
if manual {
72+
printManualMCPConfig(serverName, mcpURL)
73+
return nil
74+
}
75+
76+
claudePath, _ := exec.LookPath("claude")
77+
if claudePath == "" {
78+
pterm.Printf("claude CLI not found on PATH. Falling back to manual config.\n\n")
79+
printManualMCPConfig(serverName, mcpURL)
80+
return nil
81+
}
82+
83+
spinner, err := pterm.DefaultSpinner.Start(fmt.Sprintf("Registering MCP server %q...", serverName))
84+
if err != nil {
85+
return err
86+
}
87+
88+
out, err := exec.CommandContext(cmd.Context(), claudePath, "mcp", "add", //nolint:gosec // args are from config, not user input
89+
"--transport", "http",
90+
"--scope", scope,
91+
serverName, mcpURL,
92+
).CombinedOutput()
93+
if err != nil {
94+
spinner.Fail(fmt.Sprintf("Failed to register: %s", strings.TrimSpace(string(out))))
95+
return fmt.Errorf("claude mcp add failed: %w", err)
96+
}
97+
98+
spinner.Success(fmt.Sprintf("Registered %q (scope: %s)", serverName, scope))
99+
pterm.Printf("\nEndpoint: %s\n", mcpURL)
100+
pterm.Printf("Claude Code will handle OAuth on first connection.\n")
101+
pterm.Printf("Restart Claude Code or run /mcp to connect.\n")
102+
103+
return nil
104+
}
105+
106+
// parseTenantHost extracts the host from a C1 client-id.
107+
// Client-id format: {name}@{host}/{path}.
108+
func parseTenantHost(clientID string) (string, error) {
109+
parts := strings.SplitN(clientID, "@", 2)
110+
if len(parts) != 2 {
111+
return "", fmt.Errorf("expected format {name}@{host}/{path}")
112+
}
113+
hostPath := parts[1]
114+
hostParts := strings.SplitN(hostPath, "/", 2)
115+
if len(hostParts) != 2 {
116+
return "", fmt.Errorf("expected format {name}@{host}/{path}")
117+
}
118+
return hostParts[0], nil
119+
}
120+
121+
func printManualMCPConfig(serverName, mcpURL string) {
122+
config := map[string]any{
123+
"type": "http",
124+
"url": mcpURL,
125+
}
126+
configJSON, _ := json.MarshalIndent(map[string]any{
127+
serverName: config,
128+
}, "", " ")
129+
130+
pterm.Printf("Add the following to your Claude Code MCP config:\n\n")
131+
pterm.Printf("%s\n", configJSON)
132+
}

0 commit comments

Comments
 (0)