cudly-mcp exposes CUDly's reserved-capacity search and purchase tools (AWS EC2/RDS/ElastiCache/OpenSearch/Redshift/MemoryDB/Savings Plans, Azure VM Reservations, GCP Compute Engine CUDs) to any MCP client -- Claude Code, Claude Desktop, or another MCP-speaking agent -- as a local process. It is a thin wrapper around the shared Go packages in cloud-commitments-go; it never shells out to the CLI, and it is not deployed anywhere (see Deployment model).
Every purchase tool is dry-run by default (dry_run=true) and requires an explicit confirm=true alongside dry_run=false before it spends money. A real purchase additionally requires the operator to have started the server with CUDLY_MCP_ENABLE_REAL_PURCHASES=1 -- unset by default, so a fresh install cannot spend money until the operator opts in. See Safety model.
Use Go 1.26.6, as declared by this module. The shared Go modules are published and pinned in go.mod, so this checkout builds on its own. From this checkout:
make build
./bin/cudly-mcpmake build creates bin/cudly-mcp from ./cmd/cudly-mcp.
For local development, run the module directly or install the module's binary:
go install ./cmd/cudly-mcpThis installs the cudly-mcp binary to $(go env GOBIN) (or $(go env GOPATH)/bin if GOBIN is unset); make sure that directory is on your PATH so cudly-mcp resolves without a full path.
Or run directly without installing a binary:
go run ./cmd/cudly-mcpThis checkout has its own go.mod and no published release artifact. The shared Go modules are resolved from the versions pinned there.
The server takes no CUDly-specific configuration of its own. Each provider tool authenticates the same way the corresponding CUDly CLI path does, and every tool call also accepts a per-call override (aws_profile, azure_subscription_id, gcp_project_id) so one running server instance can serve requests against different accounts/subscriptions/projects without a restart.
One of, in the usual SDK precedence order:
AWS_PROFILE(matches a profile in~/.aws/config/~/.aws/credentials)AWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEY(+ optionalAWS_SESSION_TOKEN)- An IAM role (EC2 instance profile, ECS task role, etc.)
Per-call override: pass aws_profile on any AWS tool call to use a specific named profile for that call only.
One of:
- A service principal via
AZURE_CLIENT_ID,AZURE_CLIENT_SECRET,AZURE_TENANT_ID - Local
az loginstate (picked up byazidentity.NewDefaultAzureCredential)
Set AZURE_SUBSCRIPTION_ID to select the subscription, or pass azure_subscription_id on a per-call basis to override it.
One of:
GOOGLE_APPLICATION_CREDENTIALSpointing at a service-account JSON key file- Application Default Credentials (
gcloud auth application-default login)
Pass gcp_project_id on a per-call basis to override the ambient project.
cudly-mcpThe server speaks MCP over stdio and logs diagnostics to stderr; it does not print anything to stdout other than protocol traffic, so it is safe to launch directly from an MCP client's process-spawning config (below) rather than through a wrapper script.
By default, cudly-mcp cannot execute real purchases: dry_run=false, confirm=true calls are refused until you set CUDLY_MCP_ENABLE_REAL_PURCHASES=1 in the environment that launches the process. Add it to the env block in Register with an MCP client once you are ready to let this server spend money. See Safety model for the full rule.
Add an entry to your client's MCP server config. For Claude Code, this is ~/.claude/mcp.json. If you used make build, set command to the absolute path of ./bin/cudly-mcp. If you used go install, use the absolute path it reported: $(go env GOBIN)/cudly-mcp if GOBIN is set, otherwise $(go env GOPATH)/bin/cudly-mcp ($(go env GOBIN) expands to an empty string when GOBIN is unset, so that path alone is not a valid binary location):
{
"mcpServers": {
"cudly": {
"command": "/absolute/path/to/cudly-mcp",
"env": {
"AWS_PROFILE": "my-aws-profile",
"AZURE_SUBSCRIPTION_ID": "00000000-0000-0000-0000-000000000000"
}
}
}
}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.
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).
[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.
A typical session searches for a recommendation, previews the purchase, then executes it:
- Search: call
cudly_search_recommendationswithprovider="aws",service="ec2",region="us-east-1"to see what AWS Cost Explorer currently recommends reserving. - Preview: take a result's
region/resource_type/countand callcudly_aws_ec2_ri_purchasewith those values andterm_years/payment_optionof your choice. Leavedry_runat its default (true) -- the response validates your parameters without contacting AWS or spending anything, and reportscost/on_demand_cost/estimated_savings/savings_percentageonly when a real figure is actually known (omitted otherwise, never a fabricated0). - Execute: once the preview looks right, call the same tool again with
dry_run=false, confirm=true. This is the only combination that performs a real purchase, and only when the operator has also enabledCUDLY_MCP_ENABLE_REAL_PURCHASES=1on the server process; any other combination either previews or returns an explicit refusal error (see Safety model).
Every other provider's purchase tool (cudly_aws_savingsplans_purchase, cudly_aws_rds_ri_purchase, cudly_azure_compute_ri_purchase, cudly_gcp_computeengine_cud_purchase, ...) follows the identical dry_run-then-confirm pattern. Call cudly_list_commitment_actions at any point for the full, always-current list of tools, which ones can spend real money today, and 2-3 example prompts per tool.
dry_rundefaults totrueon every purchase tool. A dry-run call never contacts the cloud provider and never spends money -- it only validates your parameters. It reports pricing (cost/on_demand_cost/estimated_savings/savings_percentage) only when a real figure is genuinely known; those fields are omitted, not zeroed, when it isn't.dry_run=truealways previews, including whenconfirm=true. A real purchase requires bothdry_run=falseandconfirm=true;dry_run=falsewithconfirm=falseis refused with a structured error, not silently downgraded to a preview.- A real purchase also requires the operator to have set
CUDLY_MCP_ENABLE_REAL_PURCHASES=1(ortrue, case-insensitive) in the environmentcudly-mcpwas launched with. This is layered underneathconfirm:confirmonly proves the model asked to spend money, it does not prove the operator running this server wants it able to. The gate is unset (disabled) by default -- unset, empty,0,false, or any other value all disable real purchases -- so a fresh install cannot spend money until the operator explicitly opts in. When disabled, adry_run=false, confirm=truecall is refused before any provider or credential is touched, naming the flag to set. Dry runs are unaffected by this flag; they never spend regardless of its value. - A real purchase requires the target account to be named:
aws_profile(AWS) orazure_subscription_id(Azure), either as the tool argument or via the matching environment variable the provider itself reads (AWS_PROFILE,AZURE_SUBSCRIPTION_ID). GCP has no environment fallback, sogcp_project_idis required for a real CUD purchase. That is deliberate: nothing inproviders/gcpreadsGOOGLE_CLOUD_PROJECTorCLOUDSDK_CORE_PROJECT, and with no project configured the provider falls back to the first active project in yourListProjectsresponse -- an artifact of IAM visibility and API ordering rather than a project anyone chose, which is not a defensible default for spending money. Pointing the scope at an environment variable the provider ignores would be worse still: the idempotency token would name one project while the commitment landed in another. If neither is set, the real purchase is refused before any credential is touched, naming the argument to pass. Dry runs do not require it -- you can price a purchase without naming an account. This is not bookkeeping: the account is folded into the idempotency token, so leaving it to ambient credentials on one call and naming it explicitly on the next derives two different tokens for the same account. Every provider's dedupe is token-keyed, so the second call's lookup would miss and buy a second commitment. Refusing an undeterminable account makes that aliasing unreachable. Naming an account explicitly and inheriting the same value from the environment always agree, so they still dedupe normally. - Every money-affecting parameter (region, resource type, count, term, payment option, and any provider-specific dimension such as RDS's
az_config) is validated against an explicit enum or non-empty check before anything is built or sent. There is no silent default for a value that materially changes what gets purchased. - Every real purchase is tagged with a source identifying it came from this MCP server (never a user-suppliable string) and a deterministic idempotency token derived from the request's own parameters. By default, retrying an identical tool call -- however long after the original, and regardless of any clock boundary -- always derives the same token, so the provider dedupes the retry instead of buying twice; this is a fail-safe default, since the worst case of a false dedupe is a skipped intentional repeat, never a double purchase. To deliberately make a second, otherwise-identical purchase (e.g. "buy 3 RIs now" and "buy 3 more next week"), pass a fresh
idempotency_noncevalue on the second call; passing the same nonce on a retry of that same call still dedupes correctly. - Provider/SDK failures surface their full error text back to the caller; nothing is swallowed.
- A completed real purchase carries an
archerablock in the response: an optional underutilization-insurance offer (Archera covers the gap if committed capacity goes unused), the signup link, the enrollment window in days, and both partnership disclosures. It is attached only when the purchase actually succeeded, never to a dry run or a failed purchase, because neither started an enrollment window. Archera sponsors CUDly's development from a fraction of their insurance premiums, and CUDly works fully without it; both facts travel with the link in every response so a client rendering this payload cannot present the offer as a neutral recommendation. - Every real purchase writes an
mcp purchase ATTEMPTline and a matchingmcp purchase OK/mcp purchase FAILEDline to stderr, recording provider, target account, region, resource, count, term, payment option, the resulting commitment ID, and a masked idempotency token. Dry runs emit no purchase diagnostic to stderr because they spend nothing, but when auditing is enabled and the append succeeds, they are persisted in the JSONL audit log asstatus: "skipped". Capture your MCP client's stderr if you want the diagnostic trail retained. Nothing is written to stdout, which the MCP stdio transport owns for JSON-RPC framing.
Understand these before enabling real purchases, especially in a shared or production account:
- No scheduled/4-eyes approval workflow. The web UI routes a purchase through
purchase_executionswith a scheduled date and, under 4-eyes mode, a second approver who cannot be the creator. This server has no such workflow: onceCUDLY_MCP_ENABLE_REAL_PURCHASES=1is set,dry_run=falseplusconfirm=truein a single tool call executes immediately.confirmis still a guardrail against an accidental call rather than an authorization control -- it is supplied by the model driving the client, not the operator -- butCUDLY_MCP_ENABLE_REAL_PURCHASESis the operator-side authorization control layered underneath it: it must be explicitly enabled before any tool call, confirmed or not, can spend money on this server. - The persisted audit record is a local JSONL file, not CUDly's purchase history. When auditing is enabled and a write succeeds, each purchase attempt -- including previews -- is appended as one
common.AuditRecordJSON line to the path from Audit log below. An MCP purchase still does not appear in the CLI/web paths'purchase_history, so reconcile against the provider's console/billing data or this file rather than against CUDly's own database. - Credentials are whatever launched the process.
aws_profile/azure_subscription_id/gcp_project_idare per-call arguments chosen by the model, so any account reachable from the ambient credentials is reachable from any tool call. Scope the credentials you launchcudly-mcpwith to what you are willing to let it spend, rather than relying on the tool arguments to constrain it.
Auditing is on by default. When auditing is enabled and a write succeeds, each purchase attempt -- previews included -- appends one common.AuditRecord JSON line to a local file, independent of the stderr trail in Safety model above.
- Default path:
$XDG_STATE_HOME/cudly/mcp-audit.jsonl, falling back to~/.local/state/cudly/mcp-audit.jsonlwhenXDG_STATE_HOMEis unset, empty, or relative. CUDLY_MCP_AUDIT_LOGoverrides the path. Setting it to an empty or whitespace-only string disables the log entirely -- that is the explicit opt-out; leaving the variable unset is not the same thing and still uses the default path.- A preview (
dry_run=true) is recorded withstatus: "skipped",dry_run: true-- it spent nothing, but it was still a decision worth reconstructing later. - When a credential scope is supplied,
credential_scoperecords that routing identifier: an AWS profile, Azure subscription, or GCP project. It identifies how CUDly selected the target; it is not a verified provider account ID. Previews may omit it because they do not require a target. - A real purchase is recorded
"success"only when the provider reportsSuccess: truewith no embedded error. A failed provider call,Success: false, or a result containing an error is recorded as"error". - Every purchase in one server process shares a single
run_id, so a log spanning many purchases can be grouped by session. - A write failure never changes the purchase result. It logs a warning to stderr naming the path and error; the tool response returned to the caller is unaffected.
- The path is probed for read, append, and directory durability at server startup. MCP creates missing parent directories as owner-only
0700subject to umask, then opens and syncs every directory edge from the filesystem root. Each ancestor therefore needs search and read permission, every creation parent also needs write permission, and the filesystem must support directoryfsync. A path that fails these checks stops server construction instead of silently dropping every record for the session.
These are pre-existing behaviours in the underlying purchase clients, not something introduced by or specific to the MCP server -- flagged here so you know what to expect:
- Azure VM Reservations have no partial-upfront billing plan. Azure honors exactly two billing plans, all-upfront and no-upfront (billed monthly, same total price -- Azure charges no premium for spreading payments), and
cudly_azure_compute_ri_purchasedefaultspayment_optionto no-upfront when omitted.payment_option=partial-upfronthas no Azure equivalent and is rejected with an explicit error rather than silently purchased under all-upfront or no-upfront instead. - An Azure purchase with no
payment_optionnow fails loud instead of silently billing all-upfront.reservations.BillingPlanForPaymentOptionused to fall through an empty payment option to Azure's own implicit default, which is Upfront -- charging the entire commitment immediately even though nobody asked for that schedule. It now returns an explicit error naming the missing value. This is reachable outside the MCP server: the platform migration000032addedrecommendations.payment_optionasTEXT NOT NULLdefaulting to the empty string, and the platform'sinternal/purchase/execution.gopassesrec.Paymentthrough without a non-empty check, so scheduled purchases created from rows predating that migration will now fail rather than silently charge upfront. That is the intended trade (an unrequested full-upfront charge is worse than a refusal), but it is a live behaviour change: if a scheduled Azure execution starts failing with "no payment option was supplied", set the row'spayment_optionexplicitly and re-run. The MCP tool itself is unaffected --cudly_azure_compute_ri_purchasedefaultspayment_optionto no-upfront before the client is reached. - GCP Compute Engine CUDs commit resources, not instances.
cudly_gcp_computeengine_cud_purchasetakesvcpu_countandmemory_gbdirectly (a CUD is a vCPU+memory commitment), not an instance count -- there is no implicit vCPU-per-instance conversion. - AWS Savings Plans searches require term/payment/lookback;
cudly_search_recommendationsdefaults them. Unlike EC2/RDS reservation searches,GetSavingsPlansPurchaseRecommendationrejects the call unless all three are set. Whenservicetargets a Savings Plans search on AWS, the tool fills in unset fields withterm_years=1(1yr),payment_option=no-upfront,lookback_period=30d-- an explicitly supplied value is never overridden. A Savings Plans search therefore resolves to exactly one (term, payment, lookback) triple and issues exactly one Cost Explorer call. - Reservation searches fan out over every term and payment option when you omit them.
GetReservationPurchaseRecommendationaccepts exactly oneTermInYearsand onePaymentOptionper request and returns recommendations only for that cell -- there is no "give me every variant" mode. So whenterm_yearsand/orpayment_optionare omitted on an EC2/RDS/ElastiCache/etc search, the tool expands the omitted dimension to its full menu and issues one call per combination (6 when both are omitted, 2 or 3 when one is), returning the concatenated results. Each returned recommendation carries theterm/payment_optionof the request that produced it, so its money figures are attributable to a specific offer. If any one combination fails, the whole search fails rather than returning a partial menu -- five of six offers is indistinguishable from "these are all your options". Note thatlookback_periodis NOT fanned out: omitting it leaves Cost Explorer's own server-side default (7 days) in place, since the lookback window is the usage evidence behind an offer rather than another offer to choose from. The fan-out multiplies the API bill along with the coverage:cudly_search_recommendationsbuys no commitment, but Cost Explorer bills per request, so an omitted-term/omitted-payment search costs six requests rather than one (more with pagination). The tool description states this too, so a model does not treat searching as free and re-run it in a loop. regiononcudly_search_recommendationsis a client-side post-filter for AWS reservation/Savings Plans searches.GetReservationPurchaseRecommendationandGetSavingsPlansPurchaseRecommendationare account-level Cost Explorer calls with no region parameter -- AWS returns recommendations from every region the account has usage in. The tool filters the returned recommendations down toregion/include_regions(minusexclude_regions) after the call; when no region constraint is supplied, all regions are returned as before.
cudly-mcp is a local/desktop process, not a deployed service: it is intentionally kept out of the platform component's iac/, terraform/, and internal/api Lambda packaging paths. Run it on the same machine as your MCP client.
- "provider ... is not configured" / credential errors: confirm the relevant environment variable(s) from Configure credentials are set in the shell (or the client's
envblock) that launchescudly-mcp, or pass the matching per-call override (aws_profile/azure_subscription_id/gcp_project_id). - Azure/GCP purchase calls appear to hang: Azure Reservations and GCP Compute Commitments both provision asynchronously after the purchase call returns; a
success=trueresponse means the purchase request was accepted, not necessarily that the resource is already active in the portal/console. Re-runcudly_search_recommendationsor check the provider console if you need to confirm activation state. - Rate limits / throttling from the cloud provider: retry the same tool call with the same parameters -- the idempotency token guarantees a retry cannot double-purchase no matter how long you wait before retrying (see Safety model). If you genuinely want a second, separate purchase with the same parameters instead of a retry, pass a fresh
idempotency_noncevalue. - "invalid ... must be one of ..." errors: every enum-typed parameter (term, payment option, engine, az_config, sp_type, scope, tenancy, platform) is validated against an explicit allow-list; call
cudly_list_commitment_actionsor re-check this README's per-tool schema for the exact accepted values.
CUDly is maintained by LeanerCloud and licensed under the Open Software License 3.0. See the repository license and attribution files for third-party notices.