Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
bcc65db
docs(mcp): add architecture blueprint for CUDly MCP server
cristim Jul 22, 2026
1eae01f
docs(mcp): remove architecture blueprint doc
cristim Jul 22, 2026
188658c
feat(common): add cudly-mcp purchase source enum
cristim Jul 22, 2026
d503187
feat(mcp): server skeleton + list-commitment-actions tool
cristim Jul 22, 2026
a0a75f2
feat(mcp): search-recommendations tool
cristim Jul 22, 2026
1d1909a
feat(mcp): aws ec2 ri purchase tool
cristim Jul 22, 2026
072a292
feat(mcp): aws opensearch, redshift, memorydb ri purchase tools
cristim Jul 22, 2026
0ad6e0c
feat(mcp): aws rds and elasticache ri purchase tools
cristim Jul 22, 2026
8f53e92
feat(mcp): aws savings plans purchase tool
cristim Jul 22, 2026
2780c70
feat(mcp): azure vm and gcp compute engine purchase tools
cristim Jul 22, 2026
0f8e790
docs(mcp): add mcp/README.md
cristim Jul 22, 2026
3438cef
fix(mcp): resolve golangci-lint v2.10.1 findings
cristim Jul 22, 2026
6581b27
test(mcp): add end-to-end search-then-dry-run-purchase test
cristim Jul 22, 2026
febedc6
fix(mcp): derive idempotency key from every price-affecting dimension
cristim Jul 23, 2026
01fcb40
fix(mcp): omit unknown purchase cost/savings instead of reporting 0
cristim Jul 23, 2026
f2d79e8
fix(mcp): reject Azure payment_option Azure will not honor
cristim Jul 23, 2026
e4df0cf
fix(mcp): scope Azure payment_option gate to real purchases only
cristim Jul 23, 2026
0731cab
feat(mcp): expose include_regions/exclude_regions on search_recommend…
cristim Jul 23, 2026
0a657e1
fix(mcp): validate Database Savings Plan term/payment constraints
cristim Jul 23, 2026
1ad2e46
fix(mcp): properly case product names in simple RI tool descriptions
cristim Jul 23, 2026
bdb745a
docs(mcp): use go install instead of a root build artifact
cristim Jul 23, 2026
c773b0f
fix(mcp): capture decidePurchaseMode error in azure RI purchase gate
cristim Jul 23, 2026
f83bc90
docs: link to MCP server docs from root README
cristim Jul 23, 2026
a086caa
fix(azure): wire payment option into reservation billing plan
cristim Jul 23, 2026
430cc31
fix(mcp): accept and default to no-upfront on Azure RI purchase tool
cristim Jul 23, 2026
61ea519
docs(mcp): update Azure billing-plan caveat to reflect no-upfront sup…
cristim Jul 23, 2026
8c21f8a
fix(mcp): register provider factories for cudly-mcp binary
cristim Jul 23, 2026
2390f07
fix(mcp): prevent idempotency-key collisions across time-separated pu…
cristim Jul 23, 2026
741e232
fix(mcp): require instance_family for EC2Instance Savings Plans
cristim Jul 23, 2026
c6b28c6
fix(mcp): include cudly_list_commitment_actions in its own catalog
cristim Jul 23, 2026
6813df4
fix(mcp): reject whitespace-only required string fields
cristim Jul 23, 2026
629be3a
fix(mcp): populate TermYears on preview and real purchase responses
cristim Jul 23, 2026
da58bab
docs(mcp): clarify GOBIN vs GOPATH/bin binary path guidance
cristim Jul 23, 2026
026055f
style(mcp): fix golangci-lint v2.10.1 findings
cristim Jul 23, 2026
0ea6990
fix(mcp): make purchase idempotency key fail-safe (drop auto time-buc…
cristim Jul 23, 2026
12e8ca9
fix(mcp): drop partial-upfront from Azure RI payment_option schema
cristim Jul 23, 2026
6aabc82
fix(mcp): trim whitespace-only region before Savings Plans account-le…
cristim Jul 23, 2026
7f4cadc
fix(azure): wire billingPlan into SQL Database reservation purchases
cristim Jul 23, 2026
5da6c34
fix(azure): wire billingPlan into Cache for Redis reservation purchases
cristim Jul 23, 2026
913620f
fix(azure): wire billingPlan into Cosmos DB reservation purchases
cristim Jul 23, 2026
6314521
fix(azure): wire billingPlan into Search reservation purchases
cristim Jul 23, 2026
3c71e08
fix(azure): wire billingPlan into Managed Redis reservation purchases
cristim Jul 23, 2026
a3e3920
fix(azure): wire billingPlan into Synapse reservation purchases
cristim Jul 23, 2026
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ Topic pages:
- [Purchase Safety](docs/cli/purchase-safety.md) - dry-run, audit log, idempotency window, and guardrails
- [Cloud Setup](docs/cli/cloud-setup.md) - `configure-azure` and `configure-gcp` self-hosted credential bootstrap

## MCP Server

CUDly also ships an MCP server (`cudly-mcp`) that lets Claude and other MCP clients search recommendations and drive RI, Savings Plan, and CUD purchases across AWS, Azure, and GCP, with the same dry-run-by-default safety as the CLI.

Setup and usage: [mcp/README.md](mcp/README.md)

## Key Features

- **Multi-Cloud Support** - Unified interface for AWS (production), Azure (experimental), and GCP (experimental)
Expand Down
38 changes: 38 additions & 0 deletions cmd/cudly-mcp/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Command cudly-mcp runs the CUDly MCP server on stdio, exposing CUDly's
// RI/SP/CUD search and purchase tools to any MCP client (e.g. Claude Code
// via ~/.claude/mcp.json). See mcp/README.md for setup and usage.
//
// This binary is intentionally separate from the ri-helper CLI (cmd/main.go):
// it is a local/desktop MCP server, never a Lambda handler, and is kept out
// of iac/ and terraform/ (see mcp/README.md "Deployment model").
package main

import (
"context"
"log"
"os"

gosdk "github.com/modelcontextprotocol/go-sdk/mcp"

cudlymcp "github.com/LeanerCloud/CUDly/mcp"
_ "github.com/LeanerCloud/CUDly/providers/aws"
_ "github.com/LeanerCloud/CUDly/providers/azure"
_ "github.com/LeanerCloud/CUDly/providers/gcp"
)

// version is overridable at build time via:
//
// go build -ldflags "-X main.version=1.2.3" ./cmd/cudly-mcp
var version = "dev"

func main() {
server, err := cudlymcp.NewServer(version)
if err != nil {
log.Fatalf("cudly-mcp: failed to build server: %v", err)
}

if err := server.Run(context.Background(), &gosdk.StdioTransport{}); err != nil {
log.Printf("cudly-mcp: server exited with error: %v", err)
os.Exit(1)
}
}
103 changes: 103 additions & 0 deletions cmd/cudly-mcp/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package main

import (
"context"
"path/filepath"
"strings"
"testing"
"time"

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

cudlymcp "github.com/LeanerCloud/CUDly/mcp"
)

// isolateFromAmbientAWS points the AWS SDK at deliberately nonexistent
// profile/config/credentials so config.LoadDefaultConfig cannot resolve any
// real credentials -- neither from a dev machine's ~/.aws files nor from the
// network (IMDS, ECS/EKS container credential endpoints, web identity). This
// is required for TestRealPurchasePastProviderRegistration below: that test
// drives a real (non-dry-run) purchase call, so it must be impossible for it
// to reach an actual AWS account or make an actual network call, in this or
// any other environment the test happens to run in.
func isolateFromAmbientAWS(t *testing.T) {
t.Helper()
t.Setenv("AWS_PROFILE", "cudly-mcp-regression-test-nonexistent-profile")
t.Setenv("AWS_SHARED_CREDENTIALS_FILE", filepath.Join(t.TempDir(), "no-credentials"))
t.Setenv("AWS_CONFIG_FILE", filepath.Join(t.TempDir(), "no-config"))
t.Setenv("AWS_ACCESS_KEY_ID", "")
t.Setenv("AWS_SECRET_ACCESS_KEY", "")
t.Setenv("AWS_SESSION_TOKEN", "")
t.Setenv("AWS_EC2_METADATA_DISABLED", "true")
t.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "")
t.Setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", "")
t.Setenv("AWS_ROLE_ARN", "")
t.Setenv("AWS_WEB_IDENTITY_TOKEN_FILE", "")
}

// TestRealPurchasePastProviderRegistration is the regression guard for the
// bug this file's blank imports fix: cudly-mcp never imported
// providers/aws|azure|gcp, so their init()-registered factories were never
// added to provider.CreateProvider's registry, and every real (non-dry-run)
// purchase failed at ResolveClient with "provider aws is not registered"
// before ever reaching AWS.
//
// This test MUST live in package main under cmd/cudly-mcp/ -- go test
// ./mcp/... does not catch this bug even with the blank imports reverted,
// because a test binary for the mcp or mcp/tools package never pulls in
// cmd/cudly-mcp's imports. Only a test in this package has the blank
// imports in its own dependency graph, so only here does reverting them
// actually flip provider.CreateProvider("aws") back to unregistered.
//
// The test asserts the purchase attempt gets PAST registration and fails for
// a completely different, credentials-shaped reason ("AWS is not
// configured", from providers/aws/provider.go's GetServiceClient) instead of
// "not registered". It never reaches AWS: isolateFromAmbientAWS makes
// config.LoadDefaultConfig fail to resolve the (deliberately nonexistent)
// named profile before any credential lookup or network call happens.
func TestRealPurchasePastProviderRegistration(t *testing.T) {
isolateFromAmbientAWS(t)

server, err := cudlymcp.NewServer("test-regression")
require.NoError(t, err)

clientTransport, serverTransport := gosdk.NewInMemoryTransports()

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

go func() {
_ = server.Run(ctx, serverTransport)
}()

client := gosdk.NewClient(&gosdk.Implementation{Name: "test-client"}, nil)
session, err := client.Connect(ctx, clientTransport, nil)
require.NoError(t, err)
defer session.Close()

result, err := session.CallTool(ctx, &gosdk.CallToolParams{
Name: "cudly_aws_ec2_ri_purchase",
Arguments: map[string]any{
"region": "us-east-1",
"instance_type": "m5.large",
"count": 1,
"term_years": 1,
"payment_option": "no-upfront",
"dry_run": false,
"confirm": true,
},
})
require.NoError(t, err, "CallTool itself must not return a transport-level error")
require.True(t, result.IsError, "a failed real purchase must surface as a tool error, not a transport error")

text := result.Content[0].(*gosdk.TextContent).Text
assert.NotContains(t, strings.ToLower(text), "not registered",
"provider must be registered for the cudly-mcp binary: got %q", text)
// providers/aws/provider.go's GetServiceClient (via AWSProvider.IsConfigured)
// returns exactly this string when config.LoadDefaultConfig cannot resolve
// the requested profile -- observed and confirmed stable in this test run.
assert.Contains(t, text, "AWS is not configured",
"expected a credentials/config-shaped failure once past registration: got %q", text)
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,11 @@ require (
github.com/coreos/go-oidc/v3 v3.18.0
github.com/go-jose/go-jose/v4 v4.1.4
github.com/golang-migrate/migrate/v4 v4.19.1
github.com/google/jsonschema-go v0.4.3
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.9.2
github.com/microsoftgraph/msgraph-sdk-go v1.99.0
github.com/modelcontextprotocol/go-sdk v1.6.1
github.com/pashagolub/pgxmock/v4 v4.9.0
github.com/testcontainers/testcontainers-go v0.42.0
github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0
Expand Down Expand Up @@ -175,12 +177,15 @@ require (
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/segmentio/asm v1.1.3 // indirect
github.com/segmentio/encoding v0.5.4 // indirect
github.com/shirou/gopsutil/v4 v4.26.3 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
github.com/std-uritemplate/std-uritemplate/go/v2 v2.0.3 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
Expand Down
12 changes: 12 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
Expand Down Expand Up @@ -296,6 +298,8 @@ github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modelcontextprotocol/go-sdk v1.6.1 h1:0zOSupjKUxPKSocPT1Wtago+mUHU2/uZ4xSOY0FGReU=
github.com/modelcontextprotocol/go-sdk v1.6.1/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
Expand All @@ -318,6 +322,10 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:Om
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=
github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
Expand Down Expand Up @@ -345,6 +353,8 @@ github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYI
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
Expand Down Expand Up @@ -387,6 +397,8 @@ golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
Expand Down
2 changes: 2 additions & 0 deletions go.work.sum
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,7 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down Expand Up @@ -697,6 +698,7 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/tools/godoc v0.1.0-deprecated/go.mod h1:qM63CriJ961IHWmnWa9CjZnBndniPt4a3CK0PVB9bIg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down
Loading
Loading