#
tokens: 2960/50000 9/9 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── Dockerfile
├── package-lock.json
├── package.json
├── README.md
├── smithery.yaml
├── src
│   ├── index.ts
│   ├── low_level_server.ts
│   └── pulumi.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
.vscode
build
node_modules
.env
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# Using MCP Server

To interact with the MCP Server, you'll need an MCP client. [Supported
clients](https://modelcontextprotocol.io/clients) include Claude Desktop, VSCode, and Cline, among others. The configuration process is similar across all of them.

Below is a sample configuration you can add to your client:
- [Cline MCP Server configuration](https://docs.cline.bot/mcp-servers/configuring-mcp-servers)
- [VS Code MCP Server configuration](https://code.visualstudio.com/docs/copilot/chat/mcp-servers)

```json
{
  "pulumi-mcp-server": {
    "command": "docker",
    "args": [
      "run",
      "-i",
      "--rm",
      "--name",
      "pulumi-mcp-server",
      "-e",
      "PULUMI_ACCESS_TOKEN",
      "dogukanakkaya/pulumi-mcp-server"
    ],
    "env": {
      "PULUMI_ACCESS_TOKEN": "${YOUR_TOKEN}"
    },
    "transportType": "stdio"
  }
}
```

```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "outDir": "./build",
    "rootDir": "./src",
    "target": "ESNext",
    "module": "NodeNext",
    "moduleResolution": "nodenext",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
FROM node:lts-alpine

WORKDIR /app

# Copy package files and install dependencies
COPY package*.json ./
RUN npm install --ignore-scripts

# Copy the rest of the code
COPY . .

# Build the TypeScript source code
RUN npm run build

# Expose port if needed (not required for stdio-based MCP)

# Run the MCP server
CMD [ "npm", "start" ]

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "pulumi-mcp-server",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node build/index.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.7.0",
    "dotenv": "^16.4.7",
    "zod": "^3.24.2"
  },
  "devDependencies": {
    "@types/node": "^22.13.10",
    "typescript": "^5.8.2"
  }
}

```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml

startCommand:
  type: stdio
  configSchema:
    # JSON Schema defining the configuration options for the MCP.
    type: object
    required:
      - pulumiAccessToken
    properties:
      pulumiAccessToken:
        type: string
        description: Pulumi API access token
    description: Configuration for starting the Pulumi MCP Server.
  commandFunction:
    # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
    |-
    (config) => ({
      command: 'npm',
      args: ['start'],
      env: { PULUMI_ACCESS_TOKEN: config.pulumiAccessToken }
    })
  exampleConfig:
    pulumiAccessToken: dummy-token-123

```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
import 'dotenv/config';
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { PulumiClient } from './pulumi.js';

const pulumiClient = new PulumiClient(process.env.PULUMI_ACCESS_TOKEN!);

const server = new McpServer({
  name: "Pulumi MCP Server",
  version: "1.0.0"
});

server.tool("create-stack",
  {
    organization: z.string().min(1, "Organization name is required"),
    project: z.string().min(1, "Project name is required"),
    stackName: z.string().min(1, "Stack name is required"),
  },
  async ({ organization, project, stackName }) => {
    await pulumiClient.createStack({ organization, project, stackName });

    return {
      content: [{ type: "text", text: `Stack ${project}/${stackName} created in ${organization}` }]
    };
  }
);

server.resource(
  "stacks",
  new ResourceTemplate("stacks://{organization}", { list: undefined }),
  async (uri, { organization }) => {
    const stacks = await pulumiClient.listStacks({ organization: organization as string });

    return {
      contents: [
        {
          uri: uri.href,
          text: JSON.stringify(stacks)
        }
      ]
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);
```

--------------------------------------------------------------------------------
/src/low_level_server.ts:
--------------------------------------------------------------------------------

```typescript
import 'dotenv/config';
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequest,
  CallToolRequestSchema,
  ListResourceTemplatesRequestSchema,
  ListToolsRequestSchema,
  ReadResourceRequestSchema
} from "@modelcontextprotocol/sdk/types.js";
import { PulumiClient } from './pulumi.js';

const pulumiClient = new PulumiClient(process.env.PULUMI_ACCESS_TOKEN!);

const server = new Server({
  name: "Pulumi MCP Server",
  version: "1.0.0"
}, {
  capabilities: {
    resources: {},
    tools: {},
  }
});

server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
  return {
    resourceTemplates: [
      {
        name: "Pulumi Stacks",
        uriTemplate: "pulumi://{organization}"
      }
    ],
  };
});

server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const uri = request.params.uri;
  const organization = uri.split("://")[1];

  const stacks = await pulumiClient.listStacks({ organization });

  return {
    contents: [
      {
        uri,
        text: JSON.stringify(stacks)
      }
    ]
  };
});

server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "create_pulumi_stack",
        description: "Create a new Pulumi stack",
        inputSchema: {
          type: "object",
          properties: {
            organization: {
              type: "string",
              description: "Organization name to create the stack in",
            },
            project: {
              type: "string",
              description: "Project name",
            },
            stackName: {
              type: "string",
              description: "Stack name",
            },
          },
          required: ["organization", "project", "stackName"],
        },
      }
    ]
  };
});

server.setRequestHandler(
  CallToolRequestSchema,
  async (request: CallToolRequest) => {
    try {
      if (!request.params.arguments) {
        throw new Error("No arguments provided");
      }

      switch (request.params.name) {
        case "create_pulumi_stack": {
          const { organization, project, stackName } = request.params.arguments as any;
          await pulumiClient.createStack({ organization, project, stackName });

          return {
            content: [{ type: "text", text: `Stack ${project}/${stackName} created in ${organization}` }],
          };
        }

        default:
          throw new Error(`Tool not found: ${request.params.name}`);
      }
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              error: error instanceof Error ? error.message : String(error),
            }),
          },
        ],
      };
    }
  },
);

const transport = new StdioServerTransport();
await server.connect(transport);
```

--------------------------------------------------------------------------------
/src/pulumi.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from 'zod';

/**
 * Pulumi API SDK
 * 
 * This module provides functions to interact with the Pulumi Cloud REST API.
 * 
 * @see https://www.pulumi.com/docs/pulumi-cloud/reference/cloud-rest-api/
 */

// Common types
export interface PulumiError {
  code: number;
  message: string;
}

export interface Stack {
  orgName: string;
  projectName: string;
  stackName: string;
  links: {
    self: string;
  };
}

const createStackSchema = z.object({
  organization: z.string().min(1, "Organization name is required"),
  project: z.string().min(1, "Project name is required"),
  stackName: z.string().min(1, "Stack name is required"),
});

export type CreateStackInput = z.infer<typeof createStackSchema>;

export interface ListStacksResponse {
  stacks: Stack[];
}

const listStacksSchema = z.object({
  organization: z.string().optional(),
  project: z.string().optional(),
  tagName: z.string().optional(),
  tagValue: z.string().optional(),
  continuationToken: z.string().optional(),
});

export type ListStacksInput = z.infer<typeof listStacksSchema>;

export class PulumiClient {
  private readonly baseUrl = 'https://api.pulumi.com';
  private token: string;

  /**
   * Creates a new Pulumi API client
   * 
   * @param token The Pulumi access token
   */
  constructor(token: string) {
    this.token = token;
  }

  /**
   * Creates a new stack in the specified organization and project
   * 
   * POST /api/stacks/{organization}/{project}
   * 
   * @param input The input parameters for creating a stack
   * @returns void
   * @see https://www.pulumi.com/docs/pulumi-cloud/reference/cloud-rest-api/#create-stack
   */
  async createStack(input: CreateStackInput) {
    try {
      createStackSchema.parse(input);

      const { organization, project, stackName } = input;

      const url = `/api/stacks/${organization}/${project}`;
      await this.request(url, {
        method: 'POST',
        body: JSON.stringify({ stackName }),
      });

      return true;
    } catch (error) {
      if (error instanceof z.ZodError) {
        throw {
          code: 400,
          message: `Validation error: ${error.errors.map(e => e.message).join(', ')}`,
        } as PulumiError;
      }

      throw error;
    }
  }

  /**
   * Lists stacks in the specified organization and project
   * 
   * GET /api/user/stacks?
   * 
   * @param input The input parameters for listing stacks
   * @returns A promise that resolves to the list of stacks
   * @see https://www.pulumi.com/docs/pulumi-cloud/reference/cloud-rest-api/#list-stacks
   */
  async listStacks(input: ListStacksInput): Promise<ListStacksResponse> {
    try {
      listStacksSchema.parse(input);

      const { organization, project, tagName, tagValue, continuationToken } = input;

      const queryParams = new URLSearchParams();
      if (organization) {
        queryParams.append('organization', organization);
      }
      if (project) {
        queryParams.append('project', project);
      }
      if (continuationToken) {
        queryParams.append('continuationToken', continuationToken);
      }
      if (tagName) {
        queryParams.append('tagName', tagName);
      }
      if (tagValue) {
        queryParams.append('tagValue', tagValue);
      }

      const url = `/api/user/stacks?${queryParams.toString()}`;

      return this.request(url);
    } catch (error) {
      if (error instanceof z.ZodError) {
        throw {
          code: 400,
          message: `Validation error: ${error.errors.map(e => e.message).join(', ')}`,
        } as PulumiError;
      }

      throw error;
    }
  }

  /**
   * Makes an HTTP request to the Pulumi API
   * 
   * @param url The URL to request
   * @param init @RequestInit
   * @returns A promise that resolves to the response data
   */
  private async request<T = unknown>(url: string, init?: RequestInit) {
    const response = await fetch(this.baseUrl.concat(url), {
      ...init,
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/vnd.pulumi+8',
        Authorization: `token ${this.token}`,
        ...init?.headers
      }
    })

    if (response.status >= 400) throw (await response.json())
    if (response.status === 204) return null as T

    return response.json() as Promise<T>
  }
}

```