@mcpctl/pulumi (0.0.2)
Installation
@mcpctl:registry=npm install @mcpctl/pulumi@0.0.2"@mcpctl/pulumi": "0.0.2"About this package
@mcpctl/pulumi
A Pulumi dynamic provider for mcpctl. Manage
mcpctl resources — llm, server, project, secret, agent, and any other
kind mcpctl supports — declaratively from any TypeScript Pulumi program.
It talks directly to mcpd's REST API (the same endpoints the mcpctl CLI
uses) and mirrors the CLI's two round-trip guarantees:
apply's name-keyed upsert (look up byname; PUT with immutable fields stripped if it exists, else POST), andget -o json's field-stripping on read, so state does not churn.
No mcpctl binary or ~/.mcpctl credentials are required on the machine
running pulumi up.
Install
npm install @mcpctl/pulumi @pulumi/pulumi
Usage
Typed Llm wrapper
import * as pulumi from "@pulumi/pulumi";
import * as mcpctl from "@mcpctl/pulumi";
const cfg = new pulumi.Config();
const mcpd = { url: cfg.require("mcpdUrl"), token: cfg.requireSecret("mcpdPat") };
new mcpctl.Llm("homelab-qwen", {
name: "qwen3-thinking", // upsert key (immutable — changing it replaces)
type: "openai", // immutable
model: servedModelName, // pulumi.Input<string> — e.g. from another resource
url: "http://litellm.nvidia-nim.svc.cluster.local:4000",
tier: "heavy",
mcpd,
});
When model (or url, tier, description, ...) changes, Pulumi issues an
in-place PUT /api/v1/llms/qwen3-thinking on the next up. Changing name or
type forces a replacement (delete-before-replace, since name is the identity).
Generic resource — any kind
new mcpctl.McpctlResource("my-server", {
kind: "server",
name: "my-server",
spec: { /* sent verbatim to mcpd; mcpd's Zod schema validates it */ },
mcpd,
});
The provider does no per-kind schema validation beyond the name shape and the required connection — it defers to mcpd. New mcpctl resource kinds work the day mcpd ships them, without a provider release.
Auth
The provider authenticates with a long-lived PAT (mcpctl_pat_…), passed as
mcpd.token (use cfg.requireSecret(...)).
Mint one with resource-wide view:llms + edit:llms bindings, e.g. via
mcpctl apply -f -:
kind: mcptoken
name: pulumi-automation
projectId: <any-project-id> # currently required (see note)
expiresAt: null # never expires
bindings:
- { role: view, resource: llms }
- { role: edit, resource: llms }
The bindings are resource-wide, so the token drives /api/v1/llms/* regardless
of the project it is attached to (the project-scope guard only gates
/api/v1/projects/*). Grant broader bindings if you manage other kinds.
Note: mcpd currently requires
projectIdwhen minting an mcptoken, so a management-scope PAT must attach to some (throwaway) project. A future mcpd change may makeprojectIdoptional for management tokens.
Notes
- Secrets in state: dynamic-provider inputs (including the token) are stored, encrypted, in Pulumi state. Use an encrypted state backend.
- Serialization: this package is CommonJS on purpose — Pulumi serializes the
provider object, and the CJS build lets its module references (
./engine,./kinds) be captured and re-required at runtime. For the same reason the package intentionally has noexportsmap: Pulumi's closure serializer reaches the internaldist/*.jssubmodules by subpath, which anexportsallowlist would block ("package.json export path ... not found"). Validate end-to-end with a realpulumi upagainst a disposable mcpd before relying on it in production.
Dependencies
Development Dependencies
| ID | Version |
|---|---|
| @pulumi/pulumi | ^3.0.0 |
Peer Dependencies
| ID | Version |
|---|---|
| @pulumi/pulumi | ^3.0.0 |