Skip to main content
Glama

mcp-prompt-engine

by vasayxtx
prompts_server_test.go25.5 kB
package main import ( "bytes" "context" "io" "log/slog" "os" "path/filepath" "testing" "time" "github.com/mark3labs/mcp-go/client" "github.com/mark3labs/mcp-go/client/transport" "github.com/mark3labs/mcp-go/mcp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) type PromptsServerTestSuite struct { suite.Suite tempDir string logger *slog.Logger } func TestTestSuite(t *testing.T) { suite.Run(t, new(PromptsServerTestSuite)) } func (s *PromptsServerTestSuite) SetupTest() { s.tempDir = s.T().TempDir() s.logger = slog.New(slog.DiscardHandler) } // TestServeStdio tests comprehensive server integration with prompts using ServeStdio func (s *PromptsServerTestSuite) TestServeStdio() { ctx := context.Background() tests := []struct { name string enableJSONArgs bool promptName string arguments map[string]string expectedContent string // If empty, only basic validation is performed description string }{ { name: "BasicFunctionality", enableJSONArgs: false, promptName: "greeting", arguments: map[string]string{"name": "John"}, expectedContent: "Hello John!\nHave a great day!", description: "Test basic functionality without JSON argument parsing", }, { name: "WithJSONArgumentParsing", enableJSONArgs: true, promptName: "conditional_greeting", arguments: map[string]string{ "name": "Alice", "show_extra_message": "false", // JSON boolean becomes actual boolean }, expectedContent: "Hello Alice!\nHave a good day.", description: "Test JSON boolean parsing - 'false' becomes boolean false", }, { name: "WithDisabledJSONArgumentParsing", enableJSONArgs: false, promptName: "conditional_greeting", arguments: map[string]string{ "name": "Bob", "show_extra_message": "false", // Remains string "false" (truthy!) }, expectedContent: "Hello Bob!\nThis is an extra message just for you.\nHave a good day.", description: "Test disabled JSON parsing - 'false' string is truthy", }, // All testdata prompts with JSON parsing enabled (exact content validation) { name: "greeting", enableJSONArgs: true, promptName: "greeting", arguments: map[string]string{"name": "TestUser"}, description: "Test greeting template", expectedContent: "Hello TestUser!\nHave a great day!", }, { name: "conditional_greeting", enableJSONArgs: true, promptName: "conditional_greeting", arguments: map[string]string{"name": "TestUser", "show_extra_message": "true"}, description: "Test conditional greeting template", expectedContent: "Hello TestUser!\nThis is an extra message just for you.\nHave a good day.", }, { name: "greeting_with_partials", enableJSONArgs: true, promptName: "greeting_with_partials", arguments: map[string]string{"name": "TestUser"}, description: "Test greeting template with partials", expectedContent: "Hello TestUser!\nWelcome to the system.\nHave a great day!", }, { name: "logical_operators", enableJSONArgs: true, promptName: "logical_operators", arguments: map[string]string{ "is_admin": "true", "has_permission": "true", "resource": "admin_panel", "show_warning": "true", "show_error": "false", "message": "System maintenance in progress", "is_premium": "true", "is_trial": "false", "feature_enabled": "true", "feature_name": "Advanced Analytics", "username": "TestUser", }, description: "Test template with logical operators", expectedContent: "Admin Access: You have full access to admin_panel.\nAlert: System maintenance in progress\nPremium Feature: Advanced Analytics is available.\nUser: TestUser", }, { name: "multiple_partials", enableJSONArgs: true, promptName: "multiple_partials", arguments: map[string]string{ "name": "TestUser", "title": "Test Title", "author": "Test Author", "description": "This is a test description for the template", "version": "v1.0.0", }, description: "Test template with multiple partials", expectedContent: "# Test Title\nCreated by: Test Author\n## Description\nThis is a test description for the template\n## Details\nThis is a test template with multiple partials.\nHello TestUser!\nVersion: v1.0.0", }, { name: "range_scalars", enableJSONArgs: true, promptName: "range_scalars", arguments: map[string]string{ "numbers": `[1, 2, 3, 4, 5]`, "tags": `["go", "template", "test"]`, "result": "success", }, description: "Test template with range over scalars", expectedContent: "Numbers: 1 2 3 4 5 \nTags: #go #template #test \nResult: success", }, { name: "range_structs", enableJSONArgs: true, promptName: "range_structs", arguments: map[string]string{ "users": `[{"name": "Alice", "age": 30, "role": "admin"}, {"name": "Bob", "age": 25, "role": "user"}]`, "total": "2", }, description: "Test template with range over structs", expectedContent: "Users:\n - Alice (30) - admin\n - Bob (25) - user\nTotal: 2 users", }, { name: "with_object", enableJSONArgs: true, promptName: "with_object", arguments: map[string]string{ "config": `{"name": "MyApp", "version": "1.2.3", "debug": true}`, "environment": "development", }, description: "Test template with object argument", expectedContent: "Configuration:\n Name: MyApp\n Version: 1.2.3\n Debug: true\nEnvironment: development", }, } for _, tc := range tests { s.Run(tc.name, func() { // Create prompts server that will watch ./testdata directory _, mcpClient, promptsClose := s.makePromptsServerAndClient(ctx, "./testdata", tc.enableJSONArgs) defer promptsClose() // List all available prompts to verify prompt exists listResult, err := mcpClient.ListPrompts(ctx, mcp.ListPromptsRequest{}) require.NoError(s.T(), err, "ListPrompts failed for %s", tc.name) // Verify prompt exists in list var foundPrompt *mcp.Prompt for _, prompt := range listResult.Prompts { if prompt.Name == tc.promptName { foundPrompt = &prompt break } } require.NotNil(s.T(), foundPrompt, "Prompt %s not found in list", tc.promptName) // Test GetPrompt with specified arguments var getReq mcp.GetPromptRequest getReq.Params.Name = tc.promptName getReq.Params.Arguments = tc.arguments getResult, err := mcpClient.GetPrompt(ctx, getReq) require.NoError(s.T(), err, "GetPrompt failed for %s", tc.name) // Verify basic response structure assert.NotEmpty(s.T(), getResult.Description, "Expected non-empty description for %s", tc.name) require.Len(s.T(), getResult.Messages, 1, "Expected exactly 1 message for %s", tc.name) content, ok := getResult.Messages[0].Content.(mcp.TextContent) require.True(s.T(), ok, "Expected TextContent for %s", tc.name) assert.NotEmpty(s.T(), content.Text, "Expected non-empty content for %s", tc.name) actualContent := normalizeNewlines(content.Text) assert.Equal(s.T(), tc.expectedContent, actualContent, "Unexpected content for %s: %s", tc.name, tc.description) }) } } // TestParseMCPArgs tests parseMCPArgs function functionality func (s *PromptsServerTestSuite) TestParseMCPArgs() { tests := []struct { name string input map[string]string enableJSONArgs bool expected map[string]interface{} }{ { name: "empty arguments with JSON enabled", input: map[string]string{}, enableJSONArgs: true, expected: map[string]interface{}{}, }, { name: "string arguments remain strings with JSON enabled", input: map[string]string{ "name": "John", "message": "Hello World", }, enableJSONArgs: true, expected: map[string]interface{}{ "name": "John", "message": "Hello World", }, }, { name: "boolean arguments become booleans with JSON enabled", input: map[string]string{ "enabled": "true", "disabled": "false", }, enableJSONArgs: true, expected: map[string]interface{}{ "enabled": true, "disabled": false, }, }, { name: "number arguments become numbers with JSON enabled", input: map[string]string{ "count": "42", "price": "19.99", "balance": "-100.5", }, enableJSONArgs: true, expected: map[string]interface{}{ "count": float64(42), "price": 19.99, "balance": -100.5, }, }, { name: "null argument becomes nil with JSON enabled", input: map[string]string{ "optional": "null", }, enableJSONArgs: true, expected: map[string]interface{}{ "optional": nil, }, }, { name: "array arguments become arrays with JSON enabled", input: map[string]string{ "items": `["apple", "banana", "cherry"]`, "numbers": `[1, 2, 3]`, }, enableJSONArgs: true, expected: map[string]interface{}{ "items": []interface{}{"apple", "banana", "cherry"}, "numbers": []interface{}{float64(1), float64(2), float64(3)}, }, }, { name: "object arguments become objects with JSON enabled", input: map[string]string{ "user": `{"name": "Alice", "age": 30, "active": true}`, }, enableJSONArgs: true, expected: map[string]interface{}{ "user": map[string]interface{}{ "name": "Alice", "age": float64(30), "active": true, }, }, }, { name: "invalid JSON remains as strings with JSON enabled", input: map[string]string{ "invalid_json": `{name: "Alice"}`, // Missing quotes around key "incomplete": `{"name": "Alice"`, // Missing closing brace }, enableJSONArgs: true, expected: map[string]interface{}{ "invalid_json": `{name: "Alice"}`, "incomplete": `{"name": "Alice"`, }, }, { name: "all arguments remain strings when JSON disabled", input: map[string]string{ "name": "John", "enabled": "true", "count": "42", "optional": "null", "items": `["a", "b"]`, }, enableJSONArgs: false, expected: map[string]interface{}{ "name": "John", "enabled": "true", "count": "42", "optional": "null", "items": `["a", "b"]`, }, }, } for _, tt := range tests { s.Run(tt.name, func() { data := make(map[string]interface{}) parseMCPArgs(tt.input, tt.enableJSONArgs, data) assert.Equal(s.T(), tt.expected, data, "parseMCPArgs() returned unexpected result") }) } } // TestReloadPromptsNewPromptAdded tests reloadPrompts method with new prompts via ServeStdio func (s *PromptsServerTestSuite) TestReloadPromptsNewPromptAdded() { ctx := context.Background() // Create initial prompt file so ParseDir doesn't fail initialPromptFile := filepath.Join(s.tempDir, "initial_prompt.tmpl") initialPromptContent := `{{/* Initial test prompt */}} Hello {{.name}}! This is the initial prompt.` err := os.WriteFile(initialPromptFile, []byte(initialPromptContent), 0644) require.NoError(s.T(), err, "Failed to write initial prompt file") // Create prompts server that will watch the temp directory _, mcpClient, promptsClose := s.makePromptsServerAndClient(ctx, s.tempDir, true) defer promptsClose() // Verify initial prompt exists listResult, err := mcpClient.ListPrompts(ctx, mcp.ListPromptsRequest{}) require.NoError(s.T(), err, "ListPrompts failed") require.Len(s.T(), listResult.Prompts, 1, "Expected 1 prompt initially") assert.Equal(s.T(), "initial_prompt", listResult.Prompts[0].Name, "Unexpected initial prompt name") // Create a new prompt file on filesystem newPromptFile := filepath.Join(s.tempDir, "new_prompt.tmpl") newPromptContent := `{{/* New test prompt */}} Hello {{.name}}! This is a new prompt.` err = os.WriteFile(newPromptFile, []byte(newPromptContent), 0644) require.NoError(s.T(), err, "Failed to write new prompt file") // Give the client-server communication time to process the changes time.Sleep(100 * time.Millisecond) // Client should now see both prompts listResult, err = mcpClient.ListPrompts(ctx, mcp.ListPromptsRequest{}) require.NoError(s.T(), err, "ListPrompts failed after adding prompt") require.Len(s.T(), listResult.Prompts, 2, "Expected 2 prompts after adding") // Find the new prompt in the list var newPrompt *mcp.Prompt for _, prompt := range listResult.Prompts { if prompt.Name == "new_prompt" { newPrompt = &prompt break } } require.NotNil(s.T(), newPrompt, "New prompt not found in list") assert.Equal(s.T(), "New test prompt", newPrompt.Description, "Unexpected prompt description") // Verify the client can call the new prompt getReq := mcp.GetPromptRequest{} getReq.Params.Name = "new_prompt" getReq.Params.Arguments = map[string]string{"name": "Alice"} getResult, err := mcpClient.GetPrompt(ctx, getReq) require.NoError(s.T(), err, "GetPrompt failed for new prompt") require.Len(s.T(), getResult.Messages, 1, "Expected exactly 1 message") content, ok := getResult.Messages[0].Content.(mcp.TextContent) require.True(s.T(), ok, "Expected TextContent") assert.Contains(s.T(), content.Text, "Hello Alice! This is a new prompt.", "Unexpected new prompt content") } // TestReloadPromptsPromptRemoved tests reloadPrompts method with prompt removal via ServeStdio func (s *PromptsServerTestSuite) TestReloadPromptsPromptRemoved() { ctx := context.Background() // Create initial prompt file promptFile := filepath.Join(s.tempDir, "test_prompt.tmpl") promptContent := `{{/* Test prompt to be removed */}} Hello {{.name}}!` err := os.WriteFile(promptFile, []byte(promptContent), 0644) require.NoError(s.T(), err, "Failed to write test prompt file") // Create prompts server that will watch the temp directory _, mcpClient, promptsClose := s.makePromptsServerAndClient(ctx, s.tempDir, true) defer promptsClose() // Verify prompt exists initially listResult, err := mcpClient.ListPrompts(ctx, mcp.ListPromptsRequest{}) require.NoError(s.T(), err, "ListPrompts failed") require.Len(s.T(), listResult.Prompts, 1, "Expected 1 prompt initially") assert.Equal(s.T(), "test_prompt", listResult.Prompts[0].Name, "Unexpected prompt name") // Verify client can call the prompt getReq := mcp.GetPromptRequest{} getReq.Params.Name = "test_prompt" getReq.Params.Arguments = map[string]string{"name": "Bob"} _, err = mcpClient.GetPrompt(ctx, getReq) require.NoError(s.T(), err, "GetPrompt should work before removal") // Create another prompt file to avoid the empty directory issue anotherPromptFile := filepath.Join(s.tempDir, "another_prompt.tmpl") anotherPromptContent := `{{/* Another prompt that will remain */}} Greetings {{.name}}!` err = os.WriteFile(anotherPromptFile, []byte(anotherPromptContent), 0644) require.NoError(s.T(), err, "Failed to write another prompt file") // Remove the original prompt file from filesystem err = os.Remove(promptFile) require.NoError(s.T(), err, "Failed to remove prompt file") // Give the client-server communication time to process the changes time.Sleep(100 * time.Millisecond) // Client should now see only the remaining prompt listResult, err = mcpClient.ListPrompts(ctx, mcp.ListPromptsRequest{}) require.NoError(s.T(), err, "ListPrompts failed after removal") require.Len(s.T(), listResult.Prompts, 1, "Expected 1 prompt after removal") assert.Equal(s.T(), "another_prompt", listResult.Prompts[0].Name, "Expected only another_prompt to remain") // Client should get error when trying to call removed prompt _, err = mcpClient.GetPrompt(ctx, getReq) assert.Error(s.T(), err, "Expected error when getting removed prompt") // But should be able to call the remaining prompt getReq.Params.Name = "another_prompt" _, err = mcpClient.GetPrompt(ctx, getReq) require.NoError(s.T(), err, "Should be able to call remaining prompt") } // TestReloadPromptsArgumentAdded tests reloadPrompts method with argument changes via ServeStdio func (s *PromptsServerTestSuite) TestReloadPromptsArgumentAdded() { ctx := context.Background() // Create initial prompt with one argument promptFile := filepath.Join(s.tempDir, "evolving_prompt.tmpl") initialContent := `{{/* Prompt that will gain an argument */}} Hello {{.name}}!` err := os.WriteFile(promptFile, []byte(initialContent), 0644) require.NoError(s.T(), err, "Failed to write initial prompt file") // Create prompts server that will watch the temp directory _, mcpClient, promptsClose := s.makePromptsServerAndClient(ctx, s.tempDir, true) defer promptsClose() // Verify initial prompt has one argument listResult, err := mcpClient.ListPrompts(ctx, mcp.ListPromptsRequest{}) require.NoError(s.T(), err, "ListPrompts failed") require.Len(s.T(), listResult.Prompts, 1, "Expected 1 prompt initially") require.Len(s.T(), listResult.Prompts[0].Arguments, 1, "Expected 1 argument initially") assert.Equal(s.T(), "name", listResult.Prompts[0].Arguments[0].Name, "Expected 'name' argument") // Update prompt file to add new argument updatedContent := `{{/* Prompt that will gain an argument */}} Hello {{.name}}! Your age is {{.age}}.` err = os.WriteFile(promptFile, []byte(updatedContent), 0644) require.NoError(s.T(), err, "Failed to update prompt file") // Give the client-server communication time to process the changes time.Sleep(100 * time.Millisecond) // Client should now see the prompt with two arguments listResult, err = mcpClient.ListPrompts(ctx, mcp.ListPromptsRequest{}) require.NoError(s.T(), err, "ListPrompts failed after argument addition") require.Len(s.T(), listResult.Prompts, 1, "Expected 1 prompt after update") require.Len(s.T(), listResult.Prompts[0].Arguments, 2, "Expected 2 arguments after update") // Verify both arguments are present argNames := make([]string, len(listResult.Prompts[0].Arguments)) for i, arg := range listResult.Prompts[0].Arguments { argNames[i] = arg.Name } assert.Contains(s.T(), argNames, "name", "Expected 'name' argument") assert.Contains(s.T(), argNames, "age", "Expected 'age' argument") // Verify client can call the updated prompt with both arguments getReq := mcp.GetPromptRequest{} getReq.Params.Name = "evolving_prompt" getReq.Params.Arguments = map[string]string{"name": "Alice", "age": "25"} getResult, err := mcpClient.GetPrompt(ctx, getReq) require.NoError(s.T(), err, "GetPrompt failed for updated prompt") require.Len(s.T(), getResult.Messages, 1, "Expected exactly 1 message") content, ok := getResult.Messages[0].Content.(mcp.TextContent) require.True(s.T(), ok, "Expected TextContent") assert.Contains(s.T(), content.Text, "Hello Alice! Your age is 25.", "Unexpected updated prompt content") } // TestReloadPromptsArgumentRemoved tests reloadPrompts method with argument removal via ServeStdio func (s *PromptsServerTestSuite) TestReloadPromptsArgumentRemoved() { ctx := context.Background() // Create initial prompt with two arguments promptFile := filepath.Join(s.tempDir, "shrinking_prompt.tmpl") initialContent := `{{/* Prompt that will lose an argument */}} Hello {{.name}}! Your age is {{.age}}.` err := os.WriteFile(promptFile, []byte(initialContent), 0644) require.NoError(s.T(), err, "Failed to write initial prompt file") // Create prompts server that will watch the temp directory _, mcpClient, promptsClose := s.makePromptsServerAndClient(ctx, s.tempDir, true) defer promptsClose() // Verify initial prompt has two arguments listResult, err := mcpClient.ListPrompts(ctx, mcp.ListPromptsRequest{}) require.NoError(s.T(), err, "ListPrompts failed") require.Len(s.T(), listResult.Prompts, 1, "Expected 1 prompt initially") require.Len(s.T(), listResult.Prompts[0].Arguments, 2, "Expected 2 arguments initially") // Update prompt file to remove age argument updatedContent := `{{/* Prompt that will lose an argument */}} Hello {{.name}}!` err = os.WriteFile(promptFile, []byte(updatedContent), 0644) require.NoError(s.T(), err, "Failed to update prompt file") // Give the client-server communication time to process the changes time.Sleep(100 * time.Millisecond) // Client should now see the prompt with only one argument listResult, err = mcpClient.ListPrompts(ctx, mcp.ListPromptsRequest{}) require.NoError(s.T(), err, "ListPrompts failed after argument removal") require.Len(s.T(), listResult.Prompts, 1, "Expected 1 prompt after update") require.Len(s.T(), listResult.Prompts[0].Arguments, 1, "Expected 1 argument after update") assert.Equal(s.T(), "name", listResult.Prompts[0].Arguments[0].Name, "Expected only 'name' argument to remain") // Verify client can call the updated prompt with only the remaining argument getReq := mcp.GetPromptRequest{} getReq.Params.Name = "shrinking_prompt" getReq.Params.Arguments = map[string]string{"name": "Bob"} getResult, err := mcpClient.GetPrompt(ctx, getReq) require.NoError(s.T(), err, "GetPrompt failed for updated prompt") require.Len(s.T(), getResult.Messages, 1, "Expected exactly 1 message") content, ok := getResult.Messages[0].Content.(mcp.TextContent) require.True(s.T(), ok, "Expected TextContent") assert.Contains(s.T(), content.Text, "Hello Bob!", "Unexpected updated prompt content") assert.NotContains(s.T(), content.Text, "age", "Should not contain age reference after removal") } // TestReloadPromptsDescriptionChanged tests reloadPrompts method with description changes via ServeStdio func (s *PromptsServerTestSuite) TestReloadPromptsDescriptionChanged() { ctx := context.Background() // Create initial prompt with original description promptFile := filepath.Join(s.tempDir, "descriptive_prompt.tmpl") initialContent := `{{/* Original description */}} Hello {{.name}}!` err := os.WriteFile(promptFile, []byte(initialContent), 0644) require.NoError(s.T(), err, "Failed to write initial prompt file") // Create prompts server that will watch the temp directory _, mcpClient, promptsClose := s.makePromptsServerAndClient(ctx, s.tempDir, true) defer promptsClose() // Verify initial description listResult, err := mcpClient.ListPrompts(ctx, mcp.ListPromptsRequest{}) require.NoError(s.T(), err, "ListPrompts failed") require.Len(s.T(), listResult.Prompts, 1, "Expected 1 prompt initially") assert.Equal(s.T(), "Original description", listResult.Prompts[0].Description, "Expected original description") // Update prompt file with new description updatedContent := `{{/* Updated description with more details */}} Hello {{.name}}!` err = os.WriteFile(promptFile, []byte(updatedContent), 0644) require.NoError(s.T(), err, "Failed to update prompt file") // Give the client-server communication time to process the changes time.Sleep(100 * time.Millisecond) // Client should now see the updated description listResult, err = mcpClient.ListPrompts(ctx, mcp.ListPromptsRequest{}) require.NoError(s.T(), err, "ListPrompts failed after description change") require.Len(s.T(), listResult.Prompts, 1, "Expected 1 prompt after update") assert.Equal(s.T(), "Updated description with more details", listResult.Prompts[0].Description, "Expected updated description") // Verify client can still call the prompt and gets updated description getReq := mcp.GetPromptRequest{} getReq.Params.Name = "descriptive_prompt" getReq.Params.Arguments = map[string]string{"name": "Charlie"} getResult, err := mcpClient.GetPrompt(ctx, getReq) require.NoError(s.T(), err, "GetPrompt failed for updated prompt") require.Len(s.T(), getResult.Messages, 1, "Expected exactly 1 message") content, ok := getResult.Messages[0].Content.(mcp.TextContent) require.True(s.T(), ok, "Expected TextContent") assert.Contains(s.T(), content.Text, "Hello Charlie!", "Prompt functionality should remain the same") assert.Equal(s.T(), "Updated description with more details", getResult.Description, "GetPrompt should return updated description") } func (s *PromptsServerTestSuite) makePromptsServerAndClient( ctx context.Context, promptsDir string, enableJSONArgs bool, ) (*PromptsServer, *client.Client, func()) { var ctxCancel context.CancelFunc ctx, ctxCancel = context.WithCancel(ctx) // Create prompts server that will watch the temp directory promptsServer, err := NewPromptsServer(promptsDir, enableJSONArgs, s.logger) require.NoError(s.T(), err, "Failed to create prompts server") // Set up pipes for client-server communication serverReader, clientWriter := io.Pipe() clientReader, serverWriter := io.Pipe() // Start the server in a goroutine errChan := make(chan error, 1) go func() { errChan <- promptsServer.ServeStdio(ctx, serverReader, serverWriter) }() // Create transport and client var logBuffer bytes.Buffer transp := transport.NewIO(clientReader, clientWriter, io.NopCloser(&logBuffer)) err = transp.Start(ctx) require.NoError(s.T(), err, "Failed to start transport") mcpClient := client.NewClient(transp) // Initialize the client var initReq mcp.InitializeRequest initReq.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION _, err = mcpClient.Initialize(ctx, initReq) require.NoError(s.T(), err, "Failed to initialize client") return promptsServer, mcpClient, func() { ctxCancel() s.Require().NoError(<-errChan) s.Require().NoError(transp.Close()) s.Require().NoError(promptsServer.Close()) } }

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/vasayxtx/mcp-prompt-engine'

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