Skip to content
Merged
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
43 changes: 43 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,48 @@ jobs:
echo "::warning::Coverage is below 80% (current: ${coverage}%)"
fi

mcp-build:
name: Build MCP (${{ matrix.os }})
runs-on: ${{ matrix.os }}
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
binary_format: ELF
- os: macos-latest
binary_format: Mach-O
steps:
- name: Checkout code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
persist-credentials: false

- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ${{ env.GO_VERSION }}
cache: true

- name: Assert MCP artifact is absent
run: test ! -e bin/cudly-mcp

- name: Build MCP
run: make build-mcp VERSION=v0.0.0-version-test

- name: Assert host-native binary format
run: file bin/cudly-mcp | grep -F '${{ matrix.binary_format }}'

- name: Verify Make-built MCP version
env:
CUDLY_MCP_TEST_BINARY: ${{ github.workspace }}/bin/cudly-mcp
run: go test ./cmd/cudly-mcp -run '^TestBuiltBinaryReportsInjectedVersion$' -count=1

- name: Assert clean target includes MCP artifact
run: make -n clean | grep -Fx 'rm -f cudly bootstrap bin/cudly-server bin/cudly-mcp'

# Integration tests with real PostgreSQL
integration-tests:
name: Integration Tests
Expand Down Expand Up @@ -1023,6 +1065,7 @@ jobs:
- lint
- workflow-lint
- unit-tests
- mcp-build
- integration-tests
- docker-build
- terraform-validate
Expand Down
12 changes: 10 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: build clean test deploy help all build-server build-lambda test-unit test-integration \
.PHONY: build clean test deploy help all build-server build-lambda build-mcp test-unit test-integration \
test-coverage full-test security-scan terraform-validate docker-build \
fmt vet lint complexity complexity-report security-scan-go security-scan-docker \
security-scan-terraform terraform-fmt terraform-fmt-check iac-arm docker-test pre-commit \
Expand Down Expand Up @@ -30,6 +30,7 @@ help: ## Display available targets
@echo " build - Build the CLI"
@echo " build-server - Build the unified server"
@echo " build-lambda - Build for AWS Lambda"
@echo " build-mcp - Build the MCP server (cmd/cudly-mcp)"
@echo " test - Run all unit tests"
@echo " test-unit - Run unit tests only"
@echo " test-integration - Run integration tests with testcontainers"
Expand Down Expand Up @@ -59,6 +60,13 @@ build-server:
build-lambda:
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o bootstrap ./cmd/lambda

# Build the MCP server (see mcp/README.md). Uses the same $(LDFLAGS)/$(VERSION)
# as build-server so a tagged release reports its version in the MCP
# initialize response instead of "dev" (see cmd/cudly-mcp/main.go).
build-mcp:
mkdir -p bin
CGO_ENABLED=0 go build $(LDFLAGS) -o bin/cudly-mcp ./cmd/cudly-mcp

# Run unit tests
test: test-unit

Expand All @@ -84,7 +92,7 @@ full-test: test-unit test-integration test-coverage

# Clean build artifacts
clean:
rm -f cudly bootstrap bin/cudly-server
rm -f cudly bootstrap bin/cudly-server bin/cudly-mcp
rm -f coverage.out coverage.html
rm -f gosec-report.json trivy-report.json tfsec-report.json
go clean
Expand Down
12 changes: 8 additions & 4 deletions cmd/cudly-mcp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,17 @@ import (
_ "github.com/LeanerCloud/CUDly/providers/gcp"
)

// version is overridable at build time via:
// Version is overridable at build time via:
//
// go build -ldflags "-X main.version=1.2.3" ./cmd/cudly-mcp
var version = "dev"
// go build -ldflags "-X main.Version=1.2.3" ./cmd/cudly-mcp
//
// `make build-mcp` sets it from the same $(LDFLAGS)/$(VERSION) the other
// binaries (build-server, ...) use, so a release build's tag flows through to
// the MCP initialize response's Implementation.Version.
var Version = "dev"

func main() {
server, err := cudlymcp.NewServer(version)
server, err := cudlymcp.NewServer(Version)
if err != nil {
log.Fatalf("cudly-mcp: failed to build server: %v", err)
}
Expand Down
52 changes: 52 additions & 0 deletions cmd/cudly-mcp/version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package main

// Linker injection is exercised only by building and running a subprocess.

import (
"context"
"os"
"os/exec"
"path/filepath"
"testing"
"time"

gosdk "github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/stretchr/testify/require"
)

const (
buildTimeout = 5 * time.Minute
runTimeout = 15 * time.Second
injectedTestVersion = "v0.0.0-version-test"
)

func TestBuiltBinaryReportsInjectedVersion(t *testing.T) {
binPath := os.Getenv("CUDLY_MCP_TEST_BINARY")
if binPath == "" {
binPath = filepath.Join(t.TempDir(), "cudly-mcp")
ctx, cancel := context.WithTimeout(context.Background(), buildTimeout)
defer cancel()

cmd := exec.CommandContext(ctx, "go", "build",
"-ldflags", "-X main.Version="+injectedTestVersion,
"-o", binPath, ".")
cmd.Dir = "."
out, err := cmd.CombinedOutput()
require.NoErrorf(t, err, "build cmd/cudly-mcp with version ldflags:\n%s", out)
}

ctx, cancel := context.WithTimeout(context.Background(), runTimeout)
defer cancel()

transport := &gosdk.CommandTransport{Command: exec.CommandContext(ctx, binPath)}
client := gosdk.NewClient(&gosdk.Implementation{Name: "version-test-client"}, nil)
session, err := client.Connect(ctx, transport, nil)
require.NoError(t, err, "connect to the built binary over stdio")
defer session.Close()

result := session.InitializeResult()
require.NotNil(t, result, "InitializeResult")
require.NotNil(t, result.ServerInfo, "InitializeResult.ServerInfo")
require.Equal(t, injectedTestVersion, result.ServerInfo.Version,
"built binary must report the injected version")
}
16 changes: 16 additions & 0 deletions mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,22 @@ Add an entry to your client's MCP server config. For Claude Code, this is `~/.cl

`env` is optional -- omit it entirely to rely on whatever ambient credentials are already active in the shell that launches the client, or set only the provider(s) you actually use.

## Use with OpenAI Codex CLI

Codex CLI reads MCP server definitions from `~/.codex/config.toml`, under a `[mcp_servers.<name>]` table -- same `command`/`args`/`env` shape as Claude Code's `mcpServers` JSON, different file format. A project-scoped `.codex/config.toml` works the same way, but only if you have marked that project directory as trusted in Codex CLI -- an untrusted project's `.codex/` layer is silently ignored, so if `cudly` doesn't show up, register it in `~/.codex/config.toml` instead (or trust the project).

```toml
[mcp_servers.cudly]
command = "/absolute/path/to/cudly-mcp"
args = []

[mcp_servers.cudly.env]
AWS_PROFILE = "my-aws-profile"
AZURE_SUBSCRIPTION_ID = "00000000-0000-0000-0000-000000000000"
```

`CUDLY_MCP_ENABLE_REAL_PURCHASES` is deliberately omitted from this example -- leave it unset until you have exercised the dry-run path against this client and are ready to let it spend money; see [Safety model](#safety-model).

## Worked example

A typical session searches for a recommendation, previews the purchase, then executes it:
Expand Down
Loading