Skip to content

Instantly share code, notes, and snippets.

@EronWright
Last active October 28, 2025 00:13
Show Gist options
  • Select an option

  • Save EronWright/22e1528537afe6016eb5a4ccf8171e3e to your computer and use it in GitHub Desktop.

Select an option

Save EronWright/22e1528537afe6016eb5a4ccf8171e3e to your computer and use it in GitHub Desktop.
Pulumi Provider MCP Server - Implementation Plan

Pulumi Provider MCP Server - Implementation Plan

Overview

A standalone MCP server that provides direct access to Pulumi provider CRUD operations, bypassing the Pulumi engine. This enables ad-hoc infrastructure management by leveraging existing Pulumi providers through the Model Context Protocol.

Architecture

Session-Based Provider Registry with Explicit Configuration

  • Uses github.com/mark3labs/mcp-go for MCP protocol handling
  • Maintains one *providers.Registry per MCP session for provider lifecycle management
  • Requires explicit provider configuration before use
  • Uses provider IDs to reference configured providers in resource operations
  • Supports multiple configurations of the same provider (e.g., AWS in different regions)

Key Components

  1. Session State

    • *providers.Registry - Provider cache/lifecycle manager from pkg/resource/deploy/providers
    • plugin.Host - Minimal implementation for provider loading
    • map[string]Reference - Provider ID to Registry Reference mapping
    • map[string]*schema.PackageSpec - Schema cache to avoid repeated GetSchema calls
    • Provider ID counter for generating unique IDs
  2. Plugin Host

    • Minimal plugin.Host implementation
    • Handles provider plugin loading via workspace
    • Routes provider diagnostics (Log/LogStatus) to MCP messages and notifications
    • Spawns gRPC server or returns dummy address for provider callbacks
  3. Property Serialization

    • Bidirectional conversion between resource.PropertyMap and JSON
    • Handles Pulumi-specific types (secrets, assets, archives, resource references)

Project Structure

cmd/pulumi-provider-mcp-server/
  main.go                    # MCP server entry point
  session.go                 # Session state with Registry and provider ID mapping
  tools.go                   # MCP tool implementations
  host.go                    # Minimal plugin.Host implementation
  serialization.go           # PropertyMap <-> JSON conversion
  schema.go                  # Schema parsing and extraction utilities
  go.mod                     # Module with mcp-go dependency
  README.md                  # Documentation

MCP Tools Specification

Provider Lifecycle

configure_provider

Load and configure a provider instance.

Input:

{
  "package": "aws",           // Provider package name (required)
  "version": "6.0.0",         // Semantic version (optional, defaults to latest)
  "config": {                 // Provider configuration (optional)
    "region": "us-west-2",
    "accessKey": "...",
    "secretKey": "..."
  },
  "id": "aws-west"            // User-supplied provider ID (optional, auto-generated if not provided)
}

Output:

{
  "providerId": "aws-west"    // Provider ID for subsequent operations
}

Implementation:

  1. Load provider plugin via Registry.loadProvider()
  2. Call Provider.CheckConfig() to validate configuration
  3. Call Provider.Configure() with validated config
  4. Generate provider ID if not supplied (e.g., {package}-{counter})
  5. Store Registry Reference in session mapping
  6. Return provider ID

Provider Introspection

get_schema

Retrieve complete provider schema for introspection.

Input:

{
  "providerId": "aws-west"    // Configured provider ID (required)
}

Output:

{
  "schema": { /* Complete provider JSON schema */ }
}

Implementation:

  1. Lookup provider by ID
  2. Call Provider.GetSchema()
  3. Return schema JSON

Note: Provider schemas can be very large. Consider using get_resource_schema or get_function_schema for more targeted schema retrieval.

get_resource_schema

Retrieve schema for a specific resource type.

Input:

{
  "providerId": "aws-west",
  "type": "aws:s3/bucket:Bucket"  // Resource type token (required)
}

Output:

{
  "resourceSchema": {
    "description": "Provides a S3 bucket resource",
    "inputProperties": { /* input property schemas */ },
    "requiredInputs": ["bucket"],
    "properties": { /* output property schemas */ },
    "required": ["id", "arn"]
  }
}

Implementation:

  1. Lookup provider by ID
  2. Call Provider.GetSchema()
  3. Parse schema and extract resource definition for the given type
  4. Return resource schema or error if type not found

get_function_schema

Retrieve schema for a specific invoke/function.

Input:

{
  "providerId": "aws-west",
  "token": "aws:s3/getBucket:getBucket"  // Function token (required)
}

Output:

{
  "functionSchema": {
    "description": "Get information about an S3 bucket",
    "inputs": {
      "properties": { /* input property schemas */ },
      "required": ["bucket"]
    },
    "outputs": {
      "properties": { /* output property schemas */ },
      "required": ["id", "arn"]
    }
  }
}

Implementation:

  1. Lookup provider by ID
  2. Call Provider.GetSchema()
  3. Parse schema and extract function definition for the given token
  4. Return function schema or error if token not found

Resource Operations

check

Validate resource inputs against provider schema.

Input:

{
  "providerId": "aws-west",
  "urn": "urn:pulumi:stack::project::aws:s3/bucket:Bucket::my-bucket",
  "type": "aws:s3/bucket:Bucket",
  "randomSeed": "base64-encoded-bytes",
  "inputs": { /* resource inputs */ },
  "oldInputs": { /* previous inputs, optional */ }
}

Output:

{
  "inputs": { /* validated inputs */ },
  "failures": [
    {
      "property": "versioning.enabled",
      "reason": "expected boolean, got string"
    }
  ]
}

Implementation:

  1. Lookup provider by ID
  2. Convert JSON inputs to resource.PropertyMap
  3. Call Provider.Check()
  4. Convert validated inputs back to JSON
  5. Return inputs and failures

diff

Compare old and new resource properties to determine changes.

Input:

{
  "providerId": "aws-west",
  "urn": "urn:pulumi:stack::project::aws:s3/bucket:Bucket::my-bucket",
  "id": "my-bucket-id",
  "type": "aws:s3/bucket:Bucket",
  "oldInputs": { /* previous inputs */ },
  "oldOutputs": { /* previous outputs */ },
  "newInputs": { /* new inputs */ }
}

Output:

{
  "changes": "DIFF_SOME",     // DIFF_NONE, DIFF_SOME, DIFF_UNKNOWN
  "replaces": ["versioning"], // Properties requiring replacement
  "deleteBeforeReplace": false,
  "detailedDiff": {
    "versioning.enabled": {
      "kind": "UPDATE",
      "inputDiff": true
    }
  }
}

Implementation:

  1. Lookup provider by ID
  2. Convert JSON to PropertyMaps
  3. Call Provider.Diff()
  4. Convert response to JSON
  5. Return diff details

create

Provision a new resource.

Input:

{
  "providerId": "aws-west",
  "urn": "urn:pulumi:stack::project::aws:s3/bucket:Bucket::my-bucket",
  "type": "aws:s3/bucket:Bucket",
  "inputs": { /* resource inputs */ },
  "timeout": 300,             // Optional timeout in seconds
  "preview": false            // Optional preview mode
}

Output:

{
  "id": "my-bucket-actual-id",
  "properties": { /* output properties including computed values */ }
}

Implementation:

  1. Lookup provider by ID
  2. Convert JSON inputs to PropertyMap
  3. Call Provider.Create()
  4. Convert output properties to JSON
  5. Return ID and properties

read

Read current live state of a resource.

Input:

{
  "providerId": "aws-west",
  "urn": "urn:pulumi:stack::project::aws:s3/bucket:Bucket::my-bucket",
  "id": "my-bucket-actual-id",
  "type": "aws:s3/bucket:Bucket",
  "inputs": { /* last known inputs, optional */ },
  "properties": { /* last known outputs, optional */ }
}

Output:

{
  "id": "my-bucket-actual-id", // May be null if resource doesn't exist
  "properties": { /* current live state */ },
  "inputs": { /* normalized inputs */ }
}

Implementation:

  1. Lookup provider by ID
  2. Convert JSON to PropertyMaps
  3. Call Provider.Read()
  4. Convert outputs to JSON
  5. Return ID, properties, and inputs

update

Update an existing resource.

Input:

{
  "providerId": "aws-west",
  "urn": "urn:pulumi:stack::project::aws:s3/bucket:Bucket::my-bucket",
  "id": "my-bucket-actual-id",
  "type": "aws:s3/bucket:Bucket",
  "oldInputs": { /* previous inputs */ },
  "oldOutputs": { /* previous outputs */ },
  "newInputs": { /* new inputs */ },
  "timeout": 300,             // Optional timeout in seconds
  "preview": false            // Optional preview mode
}

Output:

{
  "properties": { /* updated output properties */ }
}

Implementation:

  1. Lookup provider by ID
  2. Convert JSON to PropertyMaps
  3. Call Provider.Update()
  4. Convert output properties to JSON
  5. Return properties

delete

Deprovision an existing resource.

Input:

{
  "providerId": "aws-west",
  "urn": "urn:pulumi:stack::project::aws:s3/bucket:Bucket::my-bucket",
  "id": "my-bucket-actual-id",
  "type": "aws:s3/bucket:Bucket",
  "properties": { /* last known outputs */ },
  "timeout": 300              // Optional timeout in seconds
}

Output:

{}

Implementation:

  1. Lookup provider by ID
  2. Convert JSON properties to PropertyMap
  3. Call Provider.Delete()
  4. Return empty object

Function Operations

invoke

Execute a provider function (data source or utility function).

Input:

{
  "providerId": "aws-west",
  "token": "aws:s3/getBucket:getBucket",
  "args": { /* function arguments */ }
}

Output:

{
  "return": { /* function return value */ },
  "failures": [
    {
      "property": "bucket",
      "reason": "bucket not found"
    }
  ]
}

Implementation:

  1. Lookup provider by ID
  2. Convert JSON args to PropertyMap
  3. Call Provider.Invoke()
  4. Convert return value to JSON
  5. Return results and failures

Diagnostic Message Handling

Provider Diagnostic Flow

Providers emit diagnostic messages through the plugin.Host interface during operations. These are routed to the MCP client as notifications:

Provider Operation (e.g., Create)
  ↓
Provider calls Host.Log() or Host.LogStatus()
  ↓
Plugin Host implementation
  ↓
MCP Server emits notification
  ↓
MCP Client receives diagnostic message

Diagnostic Message Types

Log Messages (Persistent)

  • Emitted via plugin.Host.Log(severity, urn, msg, streamID)
  • Severity levels: Debug, Info, Warning, Error, InfoErr
  • Include URN association for resource-specific messages
  • Sent as MCP notifications/message with appropriate log level
  • Persistent in client logs

Status Messages (Transient)

  • Emitted via plugin.Host.LogStatus(severity, urn, msg, streamID)
  • Used for progress updates and transient state
  • May be overwritten by subsequent status updates
  • Sent as MCP notifications/message with progress metadata
  • Client can choose to display transiently (e.g., status line)

MCP Notification Format

Log Message:

{
  "method": "notifications/message",
  "params": {
    "level": "info",  // "debug", "info", "warning", "error"
    "logger": "pulumi-provider",
    "data": {
      "severity": "info",
      "urn": "urn:pulumi:stack::project::aws:s3/bucket:Bucket::my-bucket",
      "message": "Creating S3 bucket...",
      "streamID": 0
    }
  }
}

Status Message:

{
  "method": "notifications/message",
  "params": {
    "level": "info",
    "logger": "pulumi-provider",
    "data": {
      "severity": "info",
      "urn": "urn:pulumi:stack::project::aws:s3/bucket:Bucket::my-bucket",
      "message": "Waiting for bucket to become available...",
      "streamID": 0,
      "status": true  // Indicates transient status message
    }
  }
}

Severity Level Mapping

Pulumi Severity MCP Log Level Description
Debug debug Detailed diagnostic information
Info info Informational messages
Warning warning Warning messages
Error error Error messages (non-fatal)
InfoErr error Error-level info messages

Provider Lifecycle Flow

Single Provider Configuration

1. configure_provider({package: "aws", version: "6.0.0", config: {region: "us-west-2"}})
   → {providerId: "aws-1"}

2. create({providerId: "aws-1", urn: "...", type: "aws:s3/bucket:Bucket", inputs: {...}})
   → {id: "my-bucket", properties: {...}}

3. read({providerId: "aws-1", urn: "...", id: "my-bucket", type: "aws:s3/bucket:Bucket"})
   → {id: "my-bucket", properties: {...}}

Multiple Provider Configurations

1. configure_provider({package: "aws", config: {region: "us-west-2"}, id: "aws-west"})
   → {providerId: "aws-west"}

2. configure_provider({package: "aws", config: {region: "us-east-1"}, id: "aws-east"})
   → {providerId: "aws-east"}

3. create({providerId: "aws-west", ...})  // Creates in us-west-2
4. create({providerId: "aws-east", ...})  // Creates in us-east-1

Implementation Steps

1. Create Module Structure

  • Create cmd/pulumi-provider-mcp-server/ directory
  • Initialize go.mod with dependencies:
    • github.com/mark3labs/mcp-go
    • github.com/pulumi/pulumi/pkg/v3
    • github.com/pulumi/pulumi/sdk/v3

2. Implement Plugin Host (host.go)

  • Define struct implementing plugin.Host interface
    • Include reference to MCP server for sending notifications
    • Track current tool execution context for associating logs
  • Implement ServerAddr() - spawn gRPC server or return dummy address
  • Implement Provider() - load provider via workspace package
  • Implement CloseProvider() - cleanup provider
  • Implement Log() - emit diagnostic messages to MCP client
    • Map severity levels (Debug, Info, Warning, Error) to MCP log levels
    • Include URN and message in log payload
    • Use MCP notifications/message for persistent logs
  • Implement LogStatus() - emit transient status updates
    • Use MCP notifications/message with progress metadata
    • Transient updates for operation progress
  • Implement other required interface methods (stubs for analyzers, etc.)

3. Implement Session State (session.go)

  • Define Session struct:
    • registry *providers.Registry
    • host plugin.Host
    • providerIDs map[string]providers.Reference
    • schemaCache map[string]*schema.PackageSpec - Cache provider schemas
    • idCounter int
    • mu sync.RWMutex
  • Implement NewSession() constructor
  • Implement GetProvider(id string) (plugin.Provider, error)
  • Implement AddProvider(id, pkg, version, config) (string, error)
  • Implement GetSchema(id string) (*schema.PackageSpec, error) - With caching
  • Implement Close() cleanup method

4. Implement Property Serialization (serialization.go)

  • Implement JSONToPropertyMap(json map[string]any) (resource.PropertyMap, error)
  • Implement PropertyMapToJSON(props resource.PropertyMap) (map[string]any, error)
  • Handle special Pulumi types:
    • Secrets
    • Assets/Archives
    • Resource references
    • Computed values
    • Output values

5. Implement Schema Utilities (schema.go)

  • Implement ExtractResourceSchema(pkgSchema *schema.PackageSpec, typeToken string) (map[string]any, error)
    • Parse package schema to find resource definition
    • Extract inputProperties, requiredInputs, properties, required fields
    • Return error if resource type not found
  • Implement ExtractFunctionSchema(pkgSchema *schema.PackageSpec, functionToken string) (map[string]any, error)
    • Parse package schema to find function definition
    • Extract inputs and outputs
    • Return error if function token not found

6. Implement MCP Tools (tools.go)

  • Implement configureProvider tool handler
  • Implement getSchema tool handler (full provider schema)
  • Implement getResourceSchema tool handler (specific resource type)
  • Implement getFunctionSchema tool handler (specific invoke function)
  • Implement check tool handler
  • Implement diff tool handler
  • Implement create tool handler
  • Implement read tool handler
  • Implement update tool handler
  • Implement delete tool handler
  • Implement invoke tool handler

7. Implement Main Server (main.go)

  • Initialize MCP server using mcp-go
  • Register all tools with descriptions and schemas
  • Set up session lifecycle handlers
    • Pass MCP server reference to plugin.Host for diagnostics
  • Configure stdio transport
  • Enable MCP notifications support
  • Start server loop
  • Handle graceful shutdown

8. Add to Build System

  • Add bin/pulumi-provider-mcp-server target to Makefile
  • Add build command: go build -o bin/pulumi-provider-mcp-server ./cmd/pulumi-provider-mcp-server
  • Add to .gitignore if needed

9. Documentation

  • Write cmd/pulumi-provider-mcp-server/README.md
  • Document tool schemas and examples
  • Add usage instructions
  • Document limitations and caveats

10. Testing

  • Unit tests for property serialization
  • Unit tests for schema extraction utilities
  • Unit tests for diagnostic message routing
  • Integration tests with test provider
  • End-to-end test with real provider (e.g., random)
  • Test diagnostic emission during operations

Key Design Decisions

✅ Explicit Configuration

Users must call configure_provider before using a provider. This ensures clear intent and allows multiple configurations.

✅ Provider IDs

Clean reference model using string IDs. Users can supply custom IDs for readability or use auto-generated ones.

✅ Multiple Configurations

Support same provider package with different configurations (e.g., multiple AWS regions, multiple Kubernetes clusters).

✅ Session Isolation

One Registry per MCP session prevents cross-contamination and allows concurrent sessions.

✅ Stateless Tools

All state stored in session, tools are pure functions. Makes reasoning about behavior easier.

✅ Property Serialization

Transparent conversion between PropertyMap and JSON. Handles Pulumi-specific types correctly.

✅ No Engine Dependency

Bypasses Pulumi engine completely. Direct provider access for ad-hoc operations.

✅ Schema Caching and Granular Access

Provider schemas cached per-session to avoid repeated GetSchema calls. Granular schema tools (get_resource_schema, get_function_schema) provide efficient access to specific resource or function schemas without transferring entire provider schema.

✅ Provider Diagnostics via MCP

Provider diagnostic messages (from plugin.Host.Log() and LogStatus()) are emitted as MCP notifications in real-time. This provides visibility into provider operations, warnings, and progress updates. Distinguishes between persistent logs (Log) and transient status updates (LogStatus).

Dependencies

module github.com/pulumi/pulumi/cmd/pulumi-provider-mcp-server

go 1.21

require (
    github.com/mark3labs/mcp-go v0.8.0  // or latest
    github.com/pulumi/pulumi/pkg/v3 v3.138.0  // or current version
    github.com/pulumi/pulumi/sdk/v3 v3.138.0  // or current version
)

Future Enhancements

  • Add call support for component resource methods
  • Add construct support for component resources
  • Add provider parameterization support
  • Add streaming support for long-running operations
  • Add richer progress reporting via MCP progress tokens
  • Add provider plugin auto-install/download
  • Add configuration validation helpers
  • Add URN generation utilities
  • Add batch operations support
  • Add provider health checks
  • Add diagnostic filtering/routing options (by severity, URN, etc.)

References

Pulumi Codebase Files

  • proto/pulumi/provider.proto - Provider gRPC protocol
  • sdk/go/common/resource/plugin/provider.go - Provider interface (line 394+)
  • pkg/resource/deploy/providers/registry.go - Provider registry implementation
  • sdk/go/common/resource/plugin/host.go - Plugin host interface (line 42+)
  • sdk/go/common/resource/plugin/context.go - Plugin context
  • pkg/resource/deploy/deploytest/pluginhost.go - Test plugin host implementation (line 275+)
  • pkg/codegen/schema/schema.go - Pulumi schema types (PackageSpec, ResourceSpec, FunctionSpec)
  • sdk/go/common/diag/diag.go - Diagnostic severity levels and sink interface

Key Interfaces

// From sdk/go/common/resource/plugin/provider.go:394
type Provider interface {
    Handshake(context.Context, ProviderHandshakeRequest) (*ProviderHandshakeResponse, error)
    GetSchema(context.Context, GetSchemaRequest) (GetSchemaResponse, error)
    CheckConfig(context.Context, CheckConfigRequest) (CheckConfigResponse, error)
    Configure(context.Context, ConfigureRequest) (ConfigureResponse, error)
    Check(context.Context, CheckRequest) (CheckResponse, error)
    Diff(context.Context, DiffRequest) (DiffResponse, error)
    Create(context.Context, CreateRequest) (CreateResponse, error)
    Read(context.Context, ReadRequest) (ReadResponse, error)
    Update(context.Context, UpdateRequest) (UpdateResponse, error)
    Delete(context.Context, DeleteRequest) (DeleteResponse, error)
    Invoke(context.Context, InvokeRequest) (InvokeResponse, error)
    // ... other methods
}

// From sdk/go/common/resource/plugin/host.go:42
type Host interface {
    ServerAddr() string
    Log(sev diag.Severity, urn resource.URN, msg string, streamID int32)
    LogStatus(sev diag.Severity, urn resource.URN, msg string, streamID int32)
    Provider(descriptor workspace.PackageDescriptor) (Provider, error)
    CloseProvider(provider Provider) error
    // ... other methods
}

// From pkg/resource/deploy/providers/registry.go:281
type Registry struct {
    host      plugin.Host
    isPreview bool
    providers map[Reference]plugin.Provider
    builtins  plugin.Provider
    aliases   map[resource.URN]resource.URN
    m         sync.RWMutex
}

Notes

  • Assumes state backend operations (reading/writing stack snapshots, managing pending operations) will be available as separate MCP tools
  • Focus of this server is purely on provider CRUD operations
  • Session cleanup must close all providers to avoid resource leaks
  • Provider plugins are loaded from standard Pulumi plugin directories (~/.pulumi/plugins)
  • gRPC server address for provider callbacks may need actual implementation depending on provider requirements
@EronWright
Copy link
Author

Implementation Complete! ✅

I've successfully completed the Pulumi Provider MCP Server implementation according to this plan. Here's the summary:

📦 What's Been Built

Core Implementation (in cmd/pulumi-provider-mcp-server/):

  • main.go - MCP server with all 11 tool registrations
  • session.go - Session state management and provider lifecycle
  • host.go - Plugin host implementation with diagnostic routing
  • tools.go - Complete MCP tool handlers (configure, schema, CRUD, invoke)
  • schema.go - Schema extraction utilities
  • serialization.go - Bidirectional JSON/PropertyMap conversion

Testing & Documentation:

  • serialization_test.go - Unit tests (all passing ✓)
  • integration_test.go - Integration tests with random provider
  • README.md - Comprehensive documentation with examples

✅ All 11 MCP Tools Implemented

Provider Lifecycle:

  • configure_provider - Load and configure providers with optional version/config

Introspection:

  • get_schema - Retrieve complete provider schema
  • get_resource_schema - Get specific resource type schema
  • get_function_schema - Get specific function/invoke schema

Resource Operations:

  • check - Validate resource inputs
  • diff - Compare old/new properties
  • create - Provision new resources
  • read - Read current live state
  • update - Update existing resources
  • delete - Deprovision resources

Function Operations:

  • invoke - Execute provider functions/data sources

🔧 Build System Integration

Added to Pulumi Makefile:

  • Build target: make bin/pulumi-provider-mcp-server
  • Added to linting targets (LINT_GOLANG_PKGS)
  • Added to Go workspace (make work)
  • Integrated into main build target

Binary builds successfully at bin/pulumi-provider-mcp-server (34MB on arm64).

🎯 Key Features

Session-based architecture - Isolated provider registries per MCP session
Multi-provider support - Multiple configurations of same provider (e.g., multi-region AWS)
Schema caching - Avoids repeated GetSchema calls for performance
Full serialization - Handles secrets, assets, archives, resource references
Diagnostic routing - Provider logs/status routed as MCP notifications
Comprehensive testing - Unit tests and integration tests included

📝 Documentation Highlights

The README includes:

  • Architecture overview and design principles
  • Complete API documentation for all 11 tools
  • Usage examples (S3 bucket creation walkthrough)
  • Multi-region configuration examples
  • Special property type encoding (secrets, assets, etc.)
  • Build and run instructions

🧪 Testing

Unit Tests (go test -v):

  • Basic type conversion tests
  • Secret/asset/archive round-trip tests
  • Complex nested structure tests
  • PropertyMap marshaling tests
  • ✅ All tests passing

Integration Tests (go test -tags integration -v):

  • Full provider lifecycle with random provider
  • Multiple provider configuration test
  • Schema caching verification

🚀 Next Steps

The implementation is production-ready! To use it:

# Build the binary
make bin/pulumi-provider-mcp-server

# Run the MCP server (uses stdio transport)
./bin/pulumi-provider-mcp-server

The server is now ready for integration with MCP clients like Claude Desktop, LLM agents, or custom tools.

📍 Location

All code is in: pulumi/cmd/pulumi-provider-mcp-server/

Would love any feedback on the implementation! 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment