# 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>
}
}
```