index.ts•9 kB
#!/usr/bin/env node
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { homedir } from 'os';
import { readFileSync } from 'fs';
import { join } from 'path';
// Terraform Cloud API configuration
const TF_API_BASE = 'https://app.terraform.io/api/v2';
// Read Terraform Cloud token from credentials file
function getTerraformToken(): string {
const credentialsPath = join(homedir(), '.terraform.d', 'credentials.tfrc.json');
try {
const credentials = JSON.parse(readFileSync(credentialsPath, 'utf-8'));
return credentials.credentials['app.terraform.io'].token;
} catch (error) {
throw new Error('Failed to read Terraform Cloud token from ~/.terraform.d/credentials.tfrc.json');
}
}
// Make authenticated requests to Terraform Cloud API
async function tfCloudRequest(endpoint: string): Promise<any> {
const token = getTerraformToken();
const response = await fetch(`${TF_API_BASE}${endpoint}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/vnd.api+json'
}
});
if (!response.ok) {
throw new Error(`Terraform Cloud API error: ${response.statusText}`);
}
return response.json();
}
// Create MCP server
const server = new McpServer({
name: 'terraform-cloud-server',
version: '1.0.0'
});
// Tool: Get workspace run status
server.registerTool(
'get_run_status',
{
title: 'Get Run Status',
description: 'Get the current run status for a Terraform Cloud workspace',
inputSchema: {
workspaceName: z.string().describe('Workspace name'),
organization: z.string().default('urbanmedia').describe('Organization name')
},
outputSchema: {
workspaceId: z.string(),
workspaceName: z.string(),
currentRunId: z.string().optional(),
currentRunStatus: z.string().optional(),
recentRuns: z.array(z.object({
id: z.string(),
status: z.string(),
createdAt: z.string(),
message: z.string()
}))
}
},
async ({ workspaceName, organization }) => {
try {
// Get workspace details
const workspaceData = await tfCloudRequest(`/organizations/${organization}/workspaces/${workspaceName}`);
const workspaceId = workspaceData.data.id;
// Get current run if exists
const currentRunData = workspaceData.data.relationships['current-run']?.data;
let currentRunId: string | undefined;
let currentRunStatus: string | undefined;
if (currentRunData?.id) {
currentRunId = currentRunData.id;
const runData = await tfCloudRequest(`/runs/${currentRunId}`);
currentRunStatus = runData.data.attributes.status;
}
// Get recent runs
const runsData = await tfCloudRequest(`/workspaces/${workspaceId}/runs?page[size]=5`);
const recentRuns = runsData.data.map((run: any) => ({
id: run.id,
status: run.attributes.status,
createdAt: run.attributes['created-at'],
message: run.attributes.message || 'No message'
}));
const output = {
workspaceId,
workspaceName,
currentRunId,
currentRunStatus,
recentRuns
};
return {
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
structuredContent: output
};
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
return {
content: [{ type: 'text', text: `Error: ${errorMsg}` }],
isError: true
};
}
}
);
// Tool: List workspaces
server.registerTool(
'list_workspaces',
{
title: 'List Workspaces',
description: 'List all workspaces in a Terraform Cloud organization',
inputSchema: {
organization: z.string().default('urbanmedia').describe('Organization name')
},
outputSchema: {
workspaces: z.array(z.object({
id: z.string(),
name: z.string(),
locked: z.boolean(),
executionMode: z.string(),
currentRunStatus: z.string().optional()
}))
}
},
async ({ organization }) => {
try {
const data = await tfCloudRequest(`/organizations/${organization}/workspaces`);
const workspaces = data.data.map((ws: any) => ({
id: ws.id,
name: ws.attributes.name,
locked: ws.attributes.locked,
executionMode: ws.attributes['execution-mode'],
currentRunStatus: ws.relationships['current-run']?.data ? 'active' : 'idle'
}));
const output = { workspaces };
return {
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
structuredContent: output
};
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
return {
content: [{ type: 'text', text: `Error: ${errorMsg}` }],
isError: true
};
}
}
);
// Tool: Get workspace details
server.registerTool(
'get_workspace_details',
{
title: 'Get Workspace Details',
description: 'Get detailed information about a Terraform Cloud workspace',
inputSchema: {
workspaceName: z.string().describe('Workspace name'),
organization: z.string().default('urbanmedia').describe('Organization name')
},
outputSchema: {
id: z.string(),
name: z.string(),
locked: z.boolean(),
executionMode: z.string(),
autoApply: z.boolean(),
terraformVersion: z.string(),
workingDirectory: z.string().optional(),
vcsRepo: z.object({
identifier: z.string(),
branch: z.string()
}).optional()
}
},
async ({ workspaceName, organization }) => {
try {
const data = await tfCloudRequest(`/organizations/${organization}/workspaces/${workspaceName}`);
const attrs = data.data.attributes;
const output = {
id: data.data.id,
name: attrs.name,
locked: attrs.locked,
executionMode: attrs['execution-mode'],
autoApply: attrs['auto-apply'],
terraformVersion: attrs['terraform-version'],
workingDirectory: attrs['working-directory'] || undefined,
vcsRepo: attrs['vcs-repo'] ? {
identifier: attrs['vcs-repo'].identifier,
branch: attrs['vcs-repo'].branch
} : undefined
};
return {
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
structuredContent: output
};
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
return {
content: [{ type: 'text', text: `Error: ${errorMsg}` }],
isError: true
};
}
}
);
// Tool: Get run details by ID
server.registerTool(
'get_run_details',
{
title: 'Get Run Details',
description: 'Get detailed information about a specific Terraform Cloud run by its ID',
inputSchema: {
runId: z.string().describe('Run ID (e.g., run-abc123)')
},
outputSchema: {
id: z.string(),
status: z.string(),
message: z.string(),
createdAt: z.string(),
source: z.string(),
isConfirmable: z.boolean(),
hasChanges: z.boolean(),
planStatus: z.string().optional(),
applyStatus: z.string().optional(),
workspace: z.object({
id: z.string(),
name: z.string()
}),
configurationVersion: z.object({
id: z.string(),
source: z.string()
}).optional()
}
},
async ({ runId }) => {
try {
const data = await tfCloudRequest(`/runs/${runId}`);
const attrs = data.data.attributes;
const relationships = data.data.relationships;
const output = {
id: data.data.id,
status: attrs.status,
message: attrs.message || 'No message',
createdAt: attrs['created-at'],
source: attrs.source,
isConfirmable: attrs['is-confirmable'],
hasChanges: attrs['has-changes'],
planStatus: attrs['plan-status'] || undefined,
applyStatus: attrs['apply-status'] || undefined,
workspace: {
id: relationships.workspace.data.id,
name: attrs['workspace-name'] || 'Unknown'
},
configurationVersion: relationships['configuration-version']?.data ? {
id: relationships['configuration-version'].data.id,
source: attrs['configuration-version-source'] || 'Unknown'
} : undefined
};
return {
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
structuredContent: output
};
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
return {
content: [{ type: 'text', text: `Error: ${errorMsg}` }],
isError: true
};
}
}
);
// Connect to stdio transport
const transport = new StdioServerTransport();
await server.connect(transport);