feat(pulumi): @mcpctl/pulumi — generic Pulumi provider for mcpctl
Some checks failed
CI/CD / lint (pull_request) Successful in 1m3s
CI/CD / typecheck (pull_request) Successful in 2m5s
CI/CD / test (pull_request) Successful in 1m19s
CI/CD / smoke (pull_request) Failing after 2m46s
CI/CD / build (pull_request) Successful in 2m25s
CI/CD / publish (pull_request) Has been skipped
Some checks failed
CI/CD / lint (pull_request) Successful in 1m3s
CI/CD / typecheck (pull_request) Successful in 2m5s
CI/CD / test (pull_request) Successful in 1m19s
CI/CD / smoke (pull_request) Failing after 2m46s
CI/CD / build (pull_request) Successful in 2m25s
CI/CD / publish (pull_request) Has been skipped
New workspace package: a Pulumi dynamic provider (TypeScript, in-process) that
manages mcpctl resources declaratively from any Pulumi program. Motivated by
model drift: when the deployed vLLM/LiteLLM model changes, mcpctl's llm target
must follow, and a Pulumi resource makes that automatic on `pulumi up`.
- Generic core `McpctlResource({ kind, name, spec, mcpd })` round-trips ANY
mcpctl resource kind; typed `Llm` wrapper for ergonomics.
- Engine talks direct HTTP to mcpd's REST API (ported from cli/api-client.ts),
mirroring apply's name-keyed upsert (PUT strips immutable name/type) and
get -o json's read field-stripping. No mcpctl binary needed → CI-friendly.
- CRUD/diff/check with correct replace semantics (name/type/kind/mcpd.url force
delete-before-replace), 429 retry, secret token in state.
- Validation deferred to mcpd's Zod schemas, so new resource kinds work with no
provider release.
- CommonJS build so the serialized dynamic provider's module refs are captured.
- 18 Vitest tests (mocked mcpd + Pulumi mocks); build + lint clean; README with
auth (PAT) setup and consumer example for kubernetes-deployment.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
96
src/pulumi/README.md
Normal file
96
src/pulumi/README.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# @mcpctl/pulumi
|
||||
|
||||
A [Pulumi](https://www.pulumi.com/) 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 by `name`; PUT with immutable fields
|
||||
stripped if it exists, else POST), and
|
||||
- `get -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
|
||||
|
||||
```bash
|
||||
npm install @mcpctl/pulumi @pulumi/pulumi
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Typed `Llm` wrapper
|
||||
|
||||
```ts
|
||||
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
|
||||
|
||||
```ts
|
||||
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 -`:
|
||||
|
||||
```yaml
|
||||
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 `projectId` when minting an mcptoken, so a
|
||||
> management-scope PAT must attach to some (throwaway) project. A future mcpd
|
||||
> change may make `projectId` optional 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. Validate end-to-end with a
|
||||
real `pulumi up` against a disposable mcpd before relying on it in production.
|
||||
Reference in New Issue
Block a user