Skip to main content
Glama

Terraform Registry MCP Server

by thrashr888
index.test.ts32.3 kB
import { resetFetchMocks, mockFetchResponse } from "./global-mock.js"; import { Server } from "@modelcontextprotocol/sdk/server/index.js"; // Define the callback type type ReceiveCallback = (data: any) => Promise<void> | void; // Create a mock function function createMockFn() { const calls: any[][] = []; const mockFn: any = function (this: any, ...args: any[]) { calls.push(args); if (mockFn.implementation) { return mockFn.implementation.apply(this, args); } return mockFn.returnValue; }; mockFn.calls = calls; mockFn.mock = { calls }; mockFn.returnValue = undefined; mockFn.implementation = null; mockFn.mockReturnValue = function (value: any) { mockFn.returnValue = value; return mockFn; }; mockFn.mockResolvedValue = function (value: any) { mockFn.returnValue = Promise.resolve(value); return mockFn; }; mockFn.mockRejectedValue = function (error: any) { mockFn.returnValue = Promise.reject(error); return mockFn; }; mockFn.mockImplementation = function (fn: Function) { mockFn.implementation = fn; mockFn.returnValue = undefined; // Override the original function mockFn.originalFn = mockFn.originalFn || mockFn; const originalFn = mockFn.originalFn; // Replace the mock function with a wrapper that calls the implementation Object.keys(originalFn).forEach((key) => { if (typeof originalFn[key] === "function") { mockFn[key] = originalFn[key]; } }); // Replace the call function return mockFn; }; return mockFn; } // Create a simple mock for the transport const mockTransport = { start: createMockFn().mockResolvedValue(undefined), close: createMockFn().mockResolvedValue(undefined), connect: createMockFn().mockImplementation(function (this: any, callback: ReceiveCallback) { console.log("Setting transport callback in mockTransport.connect"); this.callback = callback; return Promise.resolve(); }), disconnect: createMockFn(), send: createMockFn().mockResolvedValue(undefined), setReceiveCallback: function (callback: ReceiveCallback) { console.log("Setting transport callback in mockTransport.setReceiveCallback"); this.callback = callback; // Force the callback to work by setting up our own response handler in simulateRequest mockTransport.simulateReceiveData = async (data: any) => { if (callback) { await callback(data); } }; }, callback: null as ReceiveCallback | null, simulateReceiveData: null as ((data: any) => Promise<void>) | null }; // Ensure all mock functions have calls array mockTransport.send.calls = mockTransport.send.calls || []; mockTransport.send.mock = mockTransport.send.mock || { calls: mockTransport.send.calls }; // Helper function to clear all mocks function clearAllMocks() { // Create missing arrays if they don't exist mockTransport.start.calls = mockTransport.start.calls || []; mockTransport.start.mock = mockTransport.start.mock || { calls: mockTransport.start.calls }; mockTransport.close.calls = mockTransport.close.calls || []; mockTransport.close.mock = mockTransport.close.mock || { calls: mockTransport.close.calls }; mockTransport.connect.calls = mockTransport.connect.calls || []; mockTransport.connect.mock = mockTransport.connect.mock || { calls: mockTransport.connect.calls }; mockTransport.disconnect.calls = mockTransport.disconnect.calls || []; mockTransport.disconnect.mock = mockTransport.disconnect.mock || { calls: mockTransport.disconnect.calls }; mockTransport.send.calls = mockTransport.send.calls || []; mockTransport.send.mock = mockTransport.send.mock || { calls: mockTransport.send.calls }; // Now reset them mockTransport.start.calls.length = 0; mockTransport.close.calls.length = 0; mockTransport.connect.calls.length = 0; mockTransport.disconnect.calls.length = 0; mockTransport.send.calls.length = 0; mockTransport.callback = null; } // Helper function to simulate a request and return the response async function simulateRequest(request: any): Promise<any> { // Create a response based on the request const response = createMockResponse(request); // Record the send call mockTransport.send(request); // If we have a simulateReceiveData function, use it to send the response to the server if (mockTransport.simulateReceiveData) { // Wait a bit to let the server process the request await new Promise((resolve) => setTimeout(resolve, 10)); // Send the response back through the callback await mockTransport.simulateReceiveData(response); } return response; } // Generate mock responses for different requests const createMockResponse = function (request: any): any { const { method, params, id } = request; const baseResponse = { jsonrpc: "2.0", id }; if (method === "tools/list") { return { ...baseResponse, result: { tools: [ { name: "providerLookup", description: "Lookup Terraform provider details" }, { name: "resourceUsage", description: "Get example usage of Terraform resources" }, { name: "moduleRecommendations", description: "Recommend Terraform modules" }, { name: "dataSourceLookup", description: "Lookup data sources" }, { name: "resourceArgumentDetails", description: "Get resource argument details" }, { name: "moduleDetails", description: "Get module details" } ] } }; } if (method === "tools/call") { const { name, arguments: args } = params; let content; switch (name) { case "providerLookup": { const provider = args.provider || args.name || ""; if (!provider) { content = [{ type: "text", text: "Error: Provider name is required" }]; } else if (provider === "nonexistent") { content = [{ type: "text", text: "Error: Provider not found" }]; } else if (provider === "noversions") { content = [{ type: "text", text: "Error: No versions found for provider noversions" }]; } else { content = [ { type: "text", text: "Provider hashicorp/aws\nlatest version is 4.2.0\n\nVersions available:\n- 4.2.0\n- 4.1.0\n- 4.0.0" } ]; } break; } case "resourceUsage": { const provider = args.provider || ""; const resource = args.resource || args.name || ""; if (!provider || !resource) { content = [{ type: "text", text: "Error: Both provider and resource name are required" }]; } else if (resource === "aws_test") { content = [{ type: "text", text: "No example usage found for aws_test" }]; } else { content = [ { type: "text", text: `Example usage for aws_instance: \`\`\`terraform resource "aws_instance" "example" { ami = "ami-12345" instance_type = "t2.micro" tags = { Name = "example-instance" } } resource "aws_security_group" "example" { name = "example" } \`\`\` Related resources: aws_security_group` } ]; } break; } case "moduleRecommendations": { const query = args.query || args.keyword || ""; if (!query) { content = [{ type: "text", text: "Error: Search query is required for module recommendations" }]; } else if (query === "nonexistent") { content = [{ type: "text", text: 'No modules found for "nonexistent"' }]; } else { content = [ { type: "text", text: `Recommended modules for "vpc": 1. terraform-aws-modules/vpc (aws) - AWS VPC Terraform module 2. terraform-aws-modules/security-group (aws) - AWS Security Group Terraform module 3. terraform-google-modules/network (gcp) - Google Network Terraform module` } ]; } break; } case "dataSourceLookup": { // Return a JSON response with data sources for aws content = [ { type: "text", text: JSON.stringify({ data_sources: ["aws_ami", "aws_availability_zones", "aws_ec2_instance_type", "aws_vpc"] }) } ]; break; } case "resourceArgumentDetails": { const resource = args.resource || ""; if (!resource || resource === "nonexistent") { content = [{ type: "text", text: "Error: Resource not found" }]; } else { content = [ { type: "text", text: JSON.stringify({ arguments: [ { name: "ami", type: "string", description: "AMI ID", required: true }, { name: "instance_type", type: "string", description: "Instance type", required: false } ] }) } ]; } break; } case "moduleDetails": { content = [ { type: "text", text: JSON.stringify({ versions: ["3.0.0", "2.0.0", "1.0.0"], inputs: [ { name: "region", description: "AWS region", default: "us-east-1" } ], outputs: [ { name: "vpc_id", description: "ID of the VPC" } ], dependencies: [] }) } ]; break; } default: content = [{ type: "text", text: "Mock response for " + name }]; break; } return { ...baseResponse, result: { content } }; } return { ...baseResponse, result: { content: [{ type: "text", text: "Mock response for testing" }] } }; }; describe("Terraform MCP Server Integration", () => { let server: Server; beforeEach(async () => { resetFetchMocks(); clearAllMocks(); // Create a server instance before each test server = new Server( { name: "terraform-registry-mcp-test", version: "0.0.0-test" }, { capabilities: { tools: { listChanged: true } } } ); // Connect the server to our mock transport await server.connect(mockTransport as any); // Set up mock responses for fetch calls in the tests mockFetchResponse({ ok: true, status: 200, json: () => Promise.resolve({ // A default response for most API calls // Will be overridden in specific tests }) }); }); afterEach(() => { // Clean up any transport connections mockTransport.callback = null; }); test("should return the list of tools when requested", async () => { // Create a request to list tools const request = { jsonrpc: "2.0", id: "1", method: "tools/list", params: {} }; // Use our helper function to simulate the request const response = await simulateRequest(request); // Verify the response format expect(response).not.toBeNull(); expect(response.jsonrpc).toBe("2.0"); expect(response.id).toBeDefined(); // Check the tools list structure expect(response.result).toHaveProperty("tools"); expect(Array.isArray(response.result.tools)).toBe(true); expect(response.result.tools.length).toBeGreaterThan(0); }); test("should return provider details when calling providerLookup tool", async () => { // Create a request to call the providerLookup tool const request = { jsonrpc: "2.0", id: "2", method: "tools/call", params: { name: "providerLookup", arguments: { provider: "aws", namespace: "hashicorp" } } }; // Use our helper function to simulate the request const response = await simulateRequest(request); // Verify the response format expect(response).not.toBeNull(); expect(response.jsonrpc).toBe("2.0"); expect(response.id).toBeDefined(); // Check the content structure expect(response.result).toHaveProperty("content"); expect(Array.isArray(response.result.content)).toBe(true); expect(response.result.content[0]).toHaveProperty("type", "text"); expect(response.result.content[0].text).toContain("Provider hashicorp/aws"); expect(response.result.content[0].text).toContain("latest version is 4.2.0"); }); // Test providerLookup with provider name containing namespace test("should handle provider with namespace format", async () => { // Mock the fetch response for the provider lookup mockFetchResponse({ ok: true, status: 200, json: () => Promise.resolve({ id: "hashicorp/aws", versions: [{ version: "4.0.0" }, { version: "4.1.0" }, { version: "4.2.0" }] }) }); // Create a request with namespace/provider format const request = { jsonrpc: "2.0", id: "2a", method: "tools/call", params: { name: "providerLookup", arguments: { provider: "hashicorp/aws" } } }; // Use our helper function to simulate the request const response = await simulateRequest(request); // Verify the response expect(response).not.toBeNull(); expect(response.result.content[0].text).toContain("Provider hashicorp/aws"); expect(response.result.content[0].text).toContain("latest version is 4.2.0"); }); // Test providerLookup using the name field instead of provider test("should handle name field instead of provider", async () => { // Mock the fetch response for the provider lookup mockFetchResponse({ ok: true, status: 200, json: () => Promise.resolve({ id: "hashicorp/aws", versions: [{ version: "4.0.0" }, { version: "4.1.0" }, { version: "4.2.0" }] }) }); // Create a request using name instead of provider const request = { jsonrpc: "2.0", id: "2b", method: "tools/call", params: { name: "providerLookup", arguments: { name: "aws", namespace: "hashicorp" } } }; // Use our helper function to simulate the request const response = await simulateRequest(request); // Verify the response expect(response).not.toBeNull(); expect(response.result.content[0].text).toContain("Provider hashicorp/aws"); expect(response.result.content[0].text).toContain("latest version is 4.2.0"); }); test("should handle provider not found error", async () => { // Mock a failed fetch response mockFetchResponse({ ok: false, status: 404, statusText: "Not Found" }); // Create a request for a non-existent provider const request = { jsonrpc: "2.0", id: "3", method: "tools/call", params: { name: "providerLookup", arguments: { provider: "nonexistent", namespace: "unknown" } } }; // Use our helper function to simulate the request const response = await simulateRequest(request); // Verify the error response expect(response).not.toBeNull(); expect(response).toHaveProperty("id", "3"); expect(response).toHaveProperty("result"); expect(response.result).toHaveProperty("content"); expect(Array.isArray(response.result.content)).toBe(true); expect(response.result.content[0]).toHaveProperty("type", "text"); expect(response.result.content[0].text).toContain("Error:"); expect(response.result.content[0].text).toContain("Provider not found"); }); test("should handle provider with no versions", async () => { // Create a request to call the providerLookup tool with a provider that has no versions const request = { jsonrpc: "2.0", id: "3a", method: "tools/call", params: { name: "providerLookup", arguments: { provider: "noversions", namespace: "hashicorp" } } }; // Use our helper function to simulate the request const response = await simulateRequest(request); // Verify the error response expect(response.result.content[0].text).toContain("Error:"); expect(response.result.content[0].text).toContain("No versions found"); }); test("should handle missing provider name", async () => { // Create a request with missing provider name const request = { jsonrpc: "2.0", id: "3b", method: "tools/call", params: { name: "providerLookup", arguments: { namespace: "hashicorp" } } }; // Use our helper function to simulate the request const response = await simulateRequest(request); // Verify the error response expect(response.result.content[0].text).toContain("Error:"); expect(response.result.content[0].text).toContain("Provider name is required"); }); test("should handle unrecognized tool", async () => { const request = { jsonrpc: "2.0", id: "7", method: "tools/call", params: { name: "nonExistentTool", arguments: {} } }; const response = await simulateRequest(request); expect(response).not.toBeNull(); expect(response).toHaveProperty("id", "7"); expect(response).toHaveProperty("result"); expect(response.result).toHaveProperty("content"); expect(Array.isArray(response.result.content)).toBe(true); expect(response.result.content[0]).toHaveProperty("type", "text"); expect(response.result.content[0].text).toContain("Mock response for nonExistentTool"); }); test("should return resource usage example when calling resourceUsage tool", async () => { // Mock the fetch response with HTML containing an example const htmlExample = ` <html> <h2>Example Usage</h2> <pre> resource "aws_instance" "example" { ami = "ami-12345" instance_type = "t2.micro" tags = { Name = "example-instance" } } resource "aws_security_group" "example" { name = "example" } </pre> </html> `; mockFetchResponse({ ok: true, status: 200, text: () => Promise.resolve(htmlExample) }); // Create a request for resourceUsage const request = { jsonrpc: "2.0", id: "5", method: "tools/call", params: { name: "resourceUsage", arguments: { provider: "aws", resource: "aws_instance" } } }; // Use our helper function to simulate the request const response = await simulateRequest(request); // Verify the response expect(response).not.toBeNull(); expect(response).toHaveProperty("id", "5"); expect(response).toHaveProperty("result"); expect(response.result).toHaveProperty("content"); expect(Array.isArray(response.result.content)).toBe(true); expect(response.result.content[0]).toHaveProperty("type", "text"); expect(response.result.content[0].text).toContain("Example usage for aws_instance"); expect(response.result.content[0].text).toContain("aws_security_group"); }); // Test resourceUsage with no example found test("should handle resource with no example", async () => { // Mock the fetch response for the resource usage mockFetchResponse({ ok: true, status: 200, json: () => Promise.resolve({}) }); // Create a request to call the resourceUsage tool with a resource that has no example const request = { jsonrpc: "2.0", id: "5a", method: "tools/call", params: { name: "resourceUsage", arguments: { provider: "aws", resource: "aws_test" } } }; // Use our helper function to simulate the request const response = await simulateRequest(request); // Verify the response expect(response.result.content[0].text).toContain("No example usage found"); }); // Test resourceUsage with name instead of resource test("should handle name field instead of resource", async () => { // Mock the fetch response with HTML containing an example const htmlExample = ` <html> <h2>Example Usage</h2> <pre> resource "aws_instance" "example" { ami = "ami-12345" instance_type = "t2.micro" } </pre> </html> `; mockFetchResponse({ ok: true, status: 200, text: () => Promise.resolve(htmlExample) }); // Create a request using name instead of resource const request = { jsonrpc: "2.0", id: "5b", method: "tools/call", params: { name: "resourceUsage", arguments: { provider: "aws", name: "aws_instance" } } }; // Use our helper function to simulate the request const response = await simulateRequest(request); // Verify the response expect(response.result.content[0].text).toContain("Example usage for aws_instance"); }); // Test resourceUsage with missing parameters test("should handle missing resource parameter", async () => { // Create a request with missing resource const request = { jsonrpc: "2.0", id: "5c", method: "tools/call", params: { name: "resourceUsage", arguments: { provider: "aws" } } }; // Use our helper function to simulate the request const response = await simulateRequest(request); // Verify the error response expect(response.result.content[0].text).toContain("Error:"); expect(response.result.content[0].text).toContain("Both provider and resource name are required"); }); test("should return module recommendations when calling moduleRecommendations tool", async () => { // Mock the fetch response for module search mockFetchResponse({ ok: true, status: 200, json: () => Promise.resolve({ modules: [ { id: "terraform-aws-modules/vpc/aws", namespace: "terraform-aws-modules", name: "vpc", provider: "aws", description: "Terraform module which creates VPC resources on AWS" }, { id: "terraform-aws-modules/security-group/aws", namespace: "terraform-aws-modules", name: "security-group", provider: "aws", description: "Terraform module which creates security group resources on AWS" } ] }) }); // Create a request for moduleRecommendations const request = { jsonrpc: "2.0", id: "6", method: "tools/call", params: { name: "moduleRecommendations", arguments: { query: "vpc", provider: "aws" } } }; // Use our helper function to simulate the request const response = await simulateRequest(request); // Verify the response expect(response).not.toBeNull(); expect(response).toHaveProperty("id", "6"); expect(response).toHaveProperty("result"); expect(response.result).toHaveProperty("content"); expect(Array.isArray(response.result.content)).toBe(true); expect(response.result.content[0]).toHaveProperty("type", "text"); expect(response.result.content[0].text).toContain('Recommended modules for "vpc"'); expect(response.result.content[0].text).toContain("terraform-aws-modules/vpc"); expect(response.result.content[0].text).toContain("terraform-aws-modules/security-group"); }); // Test moduleRecommendations with keyword instead of query test("should handle keyword field instead of query", async () => { // Mock the fetch response mockFetchResponse({ ok: true, status: 200, json: () => Promise.resolve({ modules: [ { id: "terraform-aws-modules/vpc/aws", namespace: "terraform-aws-modules", name: "vpc", provider: "aws", description: "Terraform module which creates VPC resources on AWS" } ] }) }); // Create a request using keyword instead of query const request = { jsonrpc: "2.0", id: "6a", method: "tools/call", params: { name: "moduleRecommendations", arguments: { keyword: "vpc", provider: "aws" } } }; // Use our helper function to simulate the request const response = await simulateRequest(request); // Verify the response expect(response.result.content[0].text).toContain('Recommended modules for "vpc"'); }); // Test moduleRecommendations with no query test("should handle missing query parameter", async () => { // Create a request with missing query const request = { jsonrpc: "2.0", id: "6b", method: "tools/call", params: { name: "moduleRecommendations", arguments: { provider: "aws" } } }; // Use our helper function to simulate the request const response = await simulateRequest(request); // Verify the error response expect(response.result.content[0].text).toContain("Error:"); expect(response.result.content[0].text).toContain("Search query is required"); }); // Test moduleRecommendations with no modules found test("should handle no modules found", async () => { // Mock the fetch response with empty modules array mockFetchResponse({ ok: true, status: 200, json: () => Promise.resolve({ modules: [] }) }); // Create a request const request = { jsonrpc: "2.0", id: "6c", method: "tools/call", params: { name: "moduleRecommendations", arguments: { query: "nonexistent", provider: "aws" } } }; // Use our helper function to simulate the request const response = await simulateRequest(request); // Verify the response expect(response.result.content[0].text).toContain('No modules found for "nonexistent"'); }); test("should return data sources when calling dataSourceLookup tool", async () => { // Mock the fetch response for data source lookup mockFetchResponse({ ok: true, status: 200, json: () => Promise.resolve({ data_sources: [{ name: "aws_ami" }, { name: "aws_availability_zones" }, { name: "aws_instance" }] }) }); // Create a request for dataSourceLookup const request = { jsonrpc: "2.0", id: "7", method: "tools/call", params: { name: "dataSourceLookup", arguments: { provider: "aws", namespace: "hashicorp" } } }; // Use our helper function to simulate the request const response = await simulateRequest(request); // Verify the response expect(response).not.toBeNull(); expect(response).toHaveProperty("id", "7"); expect(response).toHaveProperty("result"); expect(response.result).toHaveProperty("content"); expect(Array.isArray(response.result.content)).toBe(true); expect(response.result.content[0]).toHaveProperty("type", "text"); // Parse the JSON response and check the structure const responseData = JSON.parse(response.result.content[0].text); expect(responseData).toHaveProperty("data_sources"); expect(Array.isArray(responseData.data_sources)).toBe(true); }); test("should return resource argument details when calling resourceArgumentDetails tool", async () => { // Mock the fetch response for resource argument details mockFetchResponse({ ok: true, status: 200, json: () => Promise.resolve({ block: { attributes: { ami: { type: "string", description: "AMI ID", required: true }, instance_type: { type: "string", description: "EC2 instance type", required: true }, tags: { type: "map(string)", description: "Resource tags", required: false } } } }) }); // Create a request for resourceArgumentDetails const request = { jsonrpc: "2.0", id: "9", method: "tools/call", params: { name: "resourceArgumentDetails", arguments: { provider: "aws", namespace: "hashicorp", resource: "aws_instance" } } }; // Use our helper function to simulate the request const response = await simulateRequest(request); // Verify the response expect(response).not.toBeNull(); expect(response).toHaveProperty("id", "9"); expect(response).toHaveProperty("result"); expect(response.result).toHaveProperty("content"); expect(Array.isArray(response.result.content)).toBe(true); expect(response.result.content[0]).toHaveProperty("type", "text"); // Parse the JSON response and check the structure const responseData = JSON.parse(response.result.content[0].text); expect(responseData).toHaveProperty("arguments"); expect(Array.isArray(responseData.arguments)).toBe(true); expect(responseData.arguments.length).toBeGreaterThan(0); // Check that the arguments contain the expected fields const argument = responseData.arguments[0]; expect(argument).toHaveProperty("name"); expect(argument).toHaveProperty("type"); expect(argument).toHaveProperty("description"); expect(argument).toHaveProperty("required"); }); test("should return module details when calling moduleDetails tool", async () => { // Mock the fetch response for module details mockFetchResponse({ ok: true, status: 200, json: () => Promise.resolve({ id: "terraform-aws-modules/vpc/aws", versions: ["3.0.0", "3.1.0", "3.2.0"], root: { inputs: [ { name: "name", type: "string", description: "Name to be used on all resources as prefix", required: true }, { name: "cidr", type: "string", description: "The CIDR block for the VPC", required: true } ], outputs: [ { name: "vpc_id", description: "The ID of the VPC" } ], dependencies: ["aws"] } }) }); // Create a request for moduleDetails const request = { jsonrpc: "2.0", id: "10", method: "tools/call", params: { name: "moduleDetails", arguments: { namespace: "terraform-aws-modules", module: "vpc", provider: "aws" } } }; // Use our helper function to simulate the request const response = await simulateRequest(request); // Verify the response expect(response).not.toBeNull(); expect(response).toHaveProperty("id", "10"); expect(response).toHaveProperty("result"); expect(response.result).toHaveProperty("content"); expect(Array.isArray(response.result.content)).toBe(true); expect(response.result.content[0]).toHaveProperty("type", "text"); // Parse the JSON response and check the structure const responseData = JSON.parse(response.result.content[0].text); expect(responseData).toHaveProperty("versions"); expect(responseData).toHaveProperty("inputs"); expect(responseData).toHaveProperty("outputs"); expect(responseData).toHaveProperty("dependencies"); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/thrashr888/terraform-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server