Skip to content

Commit d1f3e92

Browse files
authored
Merge pull request #155 from buildkite/feat_1password_key_loader
feat: added support for retrieving tokens directly from 1password
2 parents 752873f + e89f9c7 commit d1f3e92

File tree

3 files changed

+130
-12
lines changed

3 files changed

+130
-12
lines changed

README.md

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -481,11 +481,80 @@ This is particularly useful when you want to grant AI assistants access to query
481481

482482
| Variable | Description | Default | Usage |
483483
|----------|-------------|---------|-------|
484-
| `BUILDKITE_API_TOKEN` | Your Buildkite API access token | Required | Authentication for all API requests |
484+
| `BUILDKITE_API_TOKEN` | Your Buildkite API access token | Required* | Authentication for all API requests |
485+
| `BUILDKITE_API_TOKEN_FROM_1PASSWORD` | 1Password item reference for API token | - | Alternative to `BUILDKITE_API_TOKEN`. Format: `op://vault/item/field` |
485486
| `BUILDKITE_TOOLSETS` | Comma-separated list of toolsets to enable | `all` | Controls which tool groups are available |
486487
| `BUILDKITE_READ_ONLY` | Enable read-only mode (filters out write operations) | `false` | Security setting for AI assistants |
487488
| `HTTP_LISTEN_ADDR` | Address for HTTP server to listen on | `localhost:3000` | Used with `http` command |
488489

490+
*Either `BUILDKITE_API_TOKEN` or `BUILDKITE_API_TOKEN_FROM_1PASSWORD` must be specified, but not both.
491+
492+
---
493+
494+
## 🔐 1Password Integration
495+
496+
For enhanced security, you can store your Buildkite API token in [1Password](https://1password.com/) and reference it using the 1Password CLI instead of exposing it as a plain environment variable.
497+
498+
### Prerequisites
499+
500+
- [1Password CLI](https://developer.1password.com/docs/cli/get-started/) installed and authenticated
501+
- Your Buildkite API token stored in a 1Password item
502+
503+
### Usage
504+
505+
Instead of using `BUILDKITE_API_TOKEN`, use `BUILDKITE_API_TOKEN_FROM_1PASSWORD` with a 1Password item reference:
506+
507+
**Environment Variable:**
508+
```bash
509+
export BUILDKITE_API_TOKEN_FROM_1PASSWORD="op://Private/Buildkite API Token/credential"
510+
buildkite-mcp-server stdio
511+
```
512+
513+
**Command Line:**
514+
```bash
515+
buildkite-mcp-server stdio --api-token-from-1password="op://Private/Buildkite API Token/credential"
516+
```
517+
518+
> **Note:** The server will call `op read -n <reference>` to fetch the token. Ensure your 1Password CLI is properly authenticated before starting the server.
519+
520+
### Client Configuration Examples
521+
522+
<details>
523+
<summary>Claude Desktop with 1Password</summary>
524+
525+
```jsonc
526+
{
527+
"mcpServers": {
528+
"buildkite": {
529+
"command": "buildkite-mcp-server",
530+
"args": ["stdio"],
531+
"env": {
532+
"BUILDKITE_API_TOKEN_FROM_1PASSWORD": "op://Private/Buildkite API Token/credential"
533+
}
534+
}
535+
}
536+
}
537+
```
538+
</details>
539+
540+
<details>
541+
<summary>VS Code with 1Password</summary>
542+
543+
```jsonc
544+
{
545+
"servers": {
546+
"buildkite": {
547+
"command": "buildkite-mcp-server",
548+
"args": ["stdio"],
549+
"env": {
550+
"BUILDKITE_API_TOKEN_FROM_1PASSWORD": "op://Private/Buildkite API Token/credential"
551+
}
552+
}
553+
}
554+
}
555+
```
556+
</details>
557+
489558
---
490559

491560
<a name="tools"></a>

cmd/buildkite-mcp-server/main.go

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,17 @@ var (
2020
version = "dev"
2121

2222
cli struct {
23-
Stdio commands.StdioCmd `cmd:"" help:"stdio mcp server."`
24-
HTTP commands.HTTPCmd `cmd:"" help:"http mcp server. (pass --use-sse to use SSE transport"`
25-
Tools commands.ToolsCmd `cmd:"" help:"list available tools." hidden:""`
26-
APIToken string `help:"The Buildkite API token to use." env:"BUILDKITE_API_TOKEN"`
27-
BaseURL string `help:"The base URL of the Buildkite API to use." env:"BUILDKITE_BASE_URL" default:"https://api.buildkite.com/"`
28-
CacheURL string `help:"The blob storage URL for job logs cache." env:"BKLOG_CACHE_URL"`
29-
Debug bool `help:"Enable debug mode." env:"DEBUG"`
30-
OTELExporter string `help:"OpenTelemetry exporter to enable. Options are 'http/protobuf', 'grpc', or 'noop'." enum:"http/protobuf, grpc, noop" env:"OTEL_EXPORTER_OTLP_PROTOCOL" default:"noop"`
31-
HTTPHeaders []string `help:"Additional HTTP headers to send with every request. Format: 'Key: Value'" name:"http-header" env:"BUILDKITE_HTTP_HEADERS"`
32-
Version kong.VersionFlag
23+
Stdio commands.StdioCmd `cmd:"" help:"stdio mcp server."`
24+
HTTP commands.HTTPCmd `cmd:"" help:"http mcp server. (pass --use-sse to use SSE transport"`
25+
Tools commands.ToolsCmd `cmd:"" help:"list available tools." hidden:""`
26+
APIToken string `help:"The Buildkite API token to use." env:"BUILDKITE_API_TOKEN"`
27+
APITokenFrom1Password string `help:"The 1Password item to read the Buildkite API token from. Format: 'op://vault/item/field'" env:"BUILDKITE_API_TOKEN_FROM_1PASSWORD"`
28+
BaseURL string `help:"The base URL of the Buildkite API to use." env:"BUILDKITE_BASE_URL" default:"https://api.buildkite.com/"`
29+
CacheURL string `help:"The blob storage URL for job logs cache." env:"BKLOG_CACHE_URL"`
30+
Debug bool `help:"Enable debug mode." env:"DEBUG"`
31+
OTELExporter string `help:"OpenTelemetry exporter to enable. Options are 'http/protobuf', 'grpc', or 'noop'." enum:"http/protobuf, grpc, noop" env:"OTEL_EXPORTER_OTLP_PROTOCOL" default:"noop"`
32+
HTTPHeaders []string `help:"Additional HTTP headers to send with every request. Format: 'Key: Value'" name:"http-header" env:"BUILDKITE_HTTP_HEADERS"`
33+
Version kong.VersionFlag
3334
}
3435
)
3536

@@ -64,8 +65,14 @@ func run(ctx context.Context, cmd *kong.Context) error {
6465
// Parse additional headers into a map
6566
headers := commands.ParseHeaders(cli.HTTPHeaders)
6667

68+
// resolve the api token from either the token or 1password flag
69+
apiToken, err := commands.ResolveAPIToken(cli.APIToken, cli.APITokenFrom1Password)
70+
if err != nil {
71+
return fmt.Errorf("failed to resolve Buildkite API token: %w", err)
72+
}
73+
6774
client, err := gobuildkite.NewOpts(
68-
gobuildkite.WithTokenAuth(cli.APIToken),
75+
gobuildkite.WithTokenAuth(apiToken),
6976
gobuildkite.WithUserAgent(commands.UserAgent(version)),
7077
gobuildkite.WithHTTPClient(trace.NewHTTPClientWithHeaders(headers)),
7178
gobuildkite.WithBaseURL(cli.BaseURL),

internal/commands/command.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package commands
22

33
import (
4+
"errors"
45
"fmt"
6+
"os/exec"
57
"runtime"
68

79
buildkitelogs "github.com/buildkite/buildkite-logs"
810
gobuildkite "github.com/buildkite/go-buildkite/v4"
11+
"github.com/rs/zerolog/log"
912
)
1013

1114
type Globals struct {
@@ -20,3 +23,42 @@ func UserAgent(version string) string {
2023

2124
return fmt.Sprintf("buildkite-mcp-server/%s (%s; %s)", version, os, arch)
2225
}
26+
27+
func ResolveAPIToken(token, tokenFrom1Password string) (string, error) {
28+
if token != "" && tokenFrom1Password != "" {
29+
return "", fmt.Errorf("cannot specify both --api-token and --api-token-from-1password")
30+
}
31+
if token == "" && tokenFrom1Password == "" {
32+
return "", fmt.Errorf("must specify either --api-token or --api-token-from-1password")
33+
}
34+
if token != "" {
35+
return token, nil
36+
}
37+
38+
// Fetch the token from 1Password
39+
opToken, err := fetchTokenFrom1Password(tokenFrom1Password)
40+
if err != nil {
41+
return "", fmt.Errorf("failed to fetch API token from 1Password: %w", err)
42+
}
43+
return opToken, nil
44+
}
45+
46+
func fetchTokenFrom1Password(opID string) (string, error) {
47+
// read the token using the 1Password CLI with `-n` to avoid a trailing newline
48+
out, err := exec.Command("op", "read", "-n", opID).Output()
49+
if err != nil {
50+
return "", expandExecErr(err)
51+
}
52+
53+
log.Info().Msg("Fetched API token from 1Password")
54+
55+
return string(out), nil
56+
}
57+
58+
func expandExecErr(err error) error {
59+
var exitErr *exec.ExitError
60+
if errors.As(err, &exitErr) {
61+
return fmt.Errorf("command failed: %s", string(exitErr.Stderr))
62+
}
63+
return err
64+
}

0 commit comments

Comments
 (0)