diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-16 03:59:15 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-16 03:59:15 +0200 |
| commit | c578f367d81ce035c1f7b9c55e38101753e99de5 (patch) | |
| tree | 12ba8091731eabf6af1ee4d1f2f35a67c9481b4c /internal/mcp | |
| parent | 52938e05c1ab250cae1c19c29eaa050351559b3b (diff) | |
Split mcp/server.go (962L) into server, handlers_prompt, handlers_tool
server.go (223L): core lifecycle and routing
handlers_prompt.go (420L): prompt CRUD handlers
handlers_tool.go (335L): tool handlers and schema builders
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/mcp')
| -rw-r--r-- | internal/mcp/handlers_prompt.go | 420 | ||||
| -rw-r--r-- | internal/mcp/handlers_tool.go | 335 | ||||
| -rw-r--r-- | internal/mcp/server.go | 743 |
3 files changed, 757 insertions, 741 deletions
diff --git a/internal/mcp/handlers_prompt.go b/internal/mcp/handlers_prompt.go new file mode 100644 index 0000000..cfe3385 --- /dev/null +++ b/internal/mcp/handlers_prompt.go @@ -0,0 +1,420 @@ +// Summary: MCP prompt-related handler methods for list, get, create, update, and delete operations. +package mcp + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "codeberg.org/snonux/hexai/internal/promptstore" +) + +// handlePromptsList processes the prompts/list request. +func (s *Server) handlePromptsList(req Request) { + s.mu.RLock() + if !s.initialized { + s.mu.RUnlock() + s.sendError(req.ID, ErrCodeInvalidRequest, "Server not initialized") + return + } + s.mu.RUnlock() + + var params ListPromptsRequest + if req.Params != nil && len(req.Params) > 0 { + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + s.sendError(req.ID, ErrCodeInvalidParams, "Invalid prompts/list params") + return + } + } + + // List prompts from store + prompts, nextCursor, err := s.store.List(params.Cursor, 100) + if err != nil { + s.logger.Printf("store list error: %v", err) + s.sendError(req.ID, ErrCodeInternalError, "Failed to list prompts") + return + } + + // Convert to PromptInfo (initialize as empty slice so JSON marshals as [] not null) + infos := make([]PromptInfo, 0, len(prompts)) + for _, p := range prompts { + args := make([]PromptArgument, len(p.Arguments)) + for i, a := range p.Arguments { + args[i] = PromptArgument{ + Name: a.Name, + Description: a.Description, + Required: a.Required, + } + } + infos = append(infos, PromptInfo{ + Name: p.Name, + Title: p.Title, + Description: p.Description, + Arguments: args, + }) + } + + result := ListPromptsResult{ + Prompts: infos, + NextCursor: nextCursor, + } + + s.sendResponse(req.ID, result) +} + +// handlePromptsGet processes the prompts/get request. +func (s *Server) handlePromptsGet(req Request) { + s.mu.RLock() + if !s.initialized { + s.mu.RUnlock() + s.sendError(req.ID, ErrCodeInvalidRequest, "Server not initialized") + return + } + s.mu.RUnlock() + + var params GetPromptRequest + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + s.sendError(req.ID, ErrCodeInvalidParams, "Invalid prompts/get params") + return + } + + if params.Name == "" { + s.sendError(req.ID, ErrCodeInvalidParams, "Missing prompt name") + return + } + + // Get prompt from store + prompt, err := s.store.Get(params.Name) + if err != nil { + s.logger.Printf("store get error: %v", err) + s.sendError(req.ID, ErrCodeInvalidParams, fmt.Sprintf("Prompt not found: %s", params.Name)) + return + } + + // Render prompt with arguments + messages, err := s.renderPrompt(prompt, params.Arguments) + if err != nil { + s.logger.Printf("render error: %v", err) + s.sendError(req.ID, ErrCodeInvalidParams, err.Error()) + return + } + + result := GetPromptResult{ + Description: prompt.Description, + Messages: messages, + } + + s.sendResponse(req.ID, result) +} + +// renderPrompt substitutes template arguments in prompt messages. +// Returns error if required arguments are missing. +func (s *Server) renderPrompt(prompt *promptstore.Prompt, args map[string]string) ([]PromptMessage, error) { + // Validate required arguments + for _, arg := range prompt.Arguments { + if arg.Required { + if _, ok := args[arg.Name]; !ok { + return nil, fmt.Errorf("missing required argument: %s", arg.Name) + } + } + } + + // Render each message by substituting {{arg}} placeholders with values + var rendered []PromptMessage + for _, msg := range prompt.Messages { + text := msg.Content.Text + + for key, val := range args { + placeholder := "{{" + key + "}}" + text = strings.ReplaceAll(text, placeholder, val) + } + + rendered = append(rendered, PromptMessage{ + Role: msg.Role, + Content: MessageContent{ + Type: msg.Content.Type, + Text: text, + }, + }) + } + + return rendered, nil +} + +// handlePromptsCreate processes the prompts/create request. +// Creates a new custom prompt in user.jsonl. +func (s *Server) handlePromptsCreate(req Request) { + s.mu.RLock() + if !s.initialized { + s.mu.RUnlock() + s.sendError(req.ID, ErrCodeInvalidRequest, "Server not initialized") + return + } + s.mu.RUnlock() + + var params CreatePromptRequest + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + s.sendError(req.ID, ErrCodeInvalidParams, "Invalid prompts/create params") + return + } + + // Validate required fields + if err := validateCreateParams(params); err != nil { + s.sendError(req.ID, ErrCodeInvalidParams, err.Error()) + return + } + + // Build prompt from params + prompt := buildPromptFromCreateParams(params) + + if err := s.store.Create(prompt); err != nil { + s.logger.Printf("create prompt error: %v", err) + s.sendError(req.ID, ErrCodeInternalError, fmt.Sprintf("Failed to create prompt: %v", err)) + return + } + + result := PromptOperationResult{ + Success: true, + Message: fmt.Sprintf("Created prompt: %s", params.Name), + } + + s.logger.Printf("created prompt: %s", params.Name) + + // Sync to slash commands if enabled + if s.syncer != nil { + if err := s.syncer.SyncCreate(prompt); err != nil { + s.logger.Printf("slash command sync failed: %v", err) + } + } + + s.sendResponse(req.ID, result) + + // Notify clients that the prompt list has changed + s.sendPromptsListChangedNotification() +} + +// validateCreateParams validates required fields for prompt creation. +func validateCreateParams(params CreatePromptRequest) error { + if params.Name == "" { + return fmt.Errorf("prompt name is required") + } + if params.Title == "" { + return fmt.Errorf("prompt title is required") + } + if len(params.Messages) == 0 { + return fmt.Errorf("at least one message is required") + } + return nil +} + +// buildPromptFromCreateParams converts CreatePromptRequest to Prompt. +func buildPromptFromCreateParams(params CreatePromptRequest) *promptstore.Prompt { + prompt := &promptstore.Prompt{ + Name: params.Name, + Title: params.Title, + Description: params.Description, + Tags: params.Tags, + Created: time.Now(), + Updated: time.Now(), + } + + // Convert arguments + for _, arg := range params.Arguments { + prompt.Arguments = append(prompt.Arguments, promptstore.PromptArgument{ + Name: arg.Name, + Description: arg.Description, + Required: arg.Required, + }) + } + + // Convert messages + for _, msg := range params.Messages { + prompt.Messages = append(prompt.Messages, promptstore.PromptMessage{ + Role: msg.Role, + Content: promptstore.MessageContent{ + Type: msg.Content.Type, + Text: msg.Content.Text, + }, + }) + } + + return prompt +} + +// handlePromptsUpdate processes the prompts/update request. +// Updates an existing custom prompt in user.jsonl. +func (s *Server) handlePromptsUpdate(req Request) { + s.mu.RLock() + if !s.initialized { + s.mu.RUnlock() + s.sendError(req.ID, ErrCodeInvalidRequest, "Server not initialized") + return + } + s.mu.RUnlock() + + var params UpdatePromptRequest + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + s.sendError(req.ID, ErrCodeInvalidParams, "Invalid prompts/update params") + return + } + + if params.Name == "" { + s.sendError(req.ID, ErrCodeInvalidParams, "Prompt name is required") + return + } + + // Get existing prompt + existing, err := s.store.Get(params.Name) + if err != nil { + s.logger.Printf("get prompt error: %v", err) + s.sendError(req.ID, ErrCodeInvalidParams, fmt.Sprintf("Prompt not found: %s", params.Name)) + return + } + + // Apply updates to existing prompt + applyPromptUpdates(existing, params) + + if err := s.store.Update(existing); err != nil { + s.logger.Printf("update prompt error: %v", err) + s.sendError(req.ID, ErrCodeInternalError, fmt.Sprintf("Failed to update prompt: %v", err)) + return + } + + result := PromptOperationResult{ + Success: true, + Message: fmt.Sprintf("Updated prompt: %s", params.Name), + } + + s.logger.Printf("updated prompt: %s", params.Name) + + // Sync to slash commands if enabled + if s.syncer != nil { + if err := s.syncer.SyncUpdate(existing); err != nil { + s.logger.Printf("slash command sync failed: %v", err) + } + } + + s.sendResponse(req.ID, result) + + // Notify clients that the prompt list has changed + s.sendPromptsListChangedNotification() +} + +// applyPromptUpdates applies update parameters to an existing prompt. +// Only non-empty fields are applied; empty fields are left unchanged. +func applyPromptUpdates(existing *promptstore.Prompt, params UpdatePromptRequest) { + if params.Title != "" { + existing.Title = params.Title + } + if params.Description != "" { + existing.Description = params.Description + } + if len(params.Arguments) > 0 { + existing.Arguments = nil + for _, arg := range params.Arguments { + existing.Arguments = append(existing.Arguments, promptstore.PromptArgument{ + Name: arg.Name, + Description: arg.Description, + Required: arg.Required, + }) + } + } + if len(params.Messages) > 0 { + existing.Messages = nil + for _, msg := range params.Messages { + existing.Messages = append(existing.Messages, promptstore.PromptMessage{ + Role: msg.Role, + Content: promptstore.MessageContent{ + Type: msg.Content.Type, + Text: msg.Content.Text, + }, + }) + } + } + if len(params.Tags) > 0 { + existing.Tags = params.Tags + } + + existing.Updated = time.Now() +} + +// handlePromptsDelete processes the prompts/delete request. +// Deletes a custom prompt from user.jsonl. +func (s *Server) handlePromptsDelete(req Request) { + s.mu.RLock() + if !s.initialized { + s.mu.RUnlock() + s.sendError(req.ID, ErrCodeInvalidRequest, "Server not initialized") + return + } + s.mu.RUnlock() + + var params DeletePromptRequest + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + s.sendError(req.ID, ErrCodeInvalidParams, "Invalid prompts/delete params") + return + } + + if params.Name == "" { + s.sendError(req.ID, ErrCodeInvalidParams, "Prompt name is required") + return + } + + if err := s.store.Delete(params.Name); err != nil { + s.logger.Printf("delete prompt error: %v", err) + s.sendError(req.ID, ErrCodeInternalError, fmt.Sprintf("Failed to delete prompt: %v", err)) + return + } + + result := PromptOperationResult{ + Success: true, + Message: fmt.Sprintf("Deleted prompt: %s", params.Name), + } + + s.logger.Printf("deleted prompt: %s", params.Name) + + // Delete slash command file if enabled + if s.syncer != nil { + if err := s.syncer.Delete(params.Name); err != nil { + s.logger.Printf("slash command sync delete failed: %v", err) + } + } + + s.sendResponse(req.ID, result) + + // Notify clients that the prompt list has changed + s.sendPromptsListChangedNotification() +} + +// convertToCreatePromptRequest converts map arguments to CreatePromptRequest. +// Uses JSON marshal/unmarshal round-trip for type conversion. +func convertToCreatePromptRequest(args map[string]interface{}) (CreatePromptRequest, error) { + data, err := json.Marshal(args) + if err != nil { + return CreatePromptRequest{}, fmt.Errorf("marshal args: %w", err) + } + + var req CreatePromptRequest + if err := json.Unmarshal(data, &req); err != nil { + return CreatePromptRequest{}, fmt.Errorf("unmarshal to CreatePromptRequest: %w", err) + } + + return req, nil +} + +// convertToUpdatePromptRequest converts map arguments to UpdatePromptRequest. +// Uses JSON marshal/unmarshal round-trip for type conversion. +func convertToUpdatePromptRequest(args map[string]interface{}) (UpdatePromptRequest, error) { + data, err := json.Marshal(args) + if err != nil { + return UpdatePromptRequest{}, fmt.Errorf("marshal args: %w", err) + } + + var req UpdatePromptRequest + if err := json.Unmarshal(data, &req); err != nil { + return UpdatePromptRequest{}, fmt.Errorf("unmarshal to UpdatePromptRequest: %w", err) + } + + return req, nil +} diff --git a/internal/mcp/handlers_tool.go b/internal/mcp/handlers_tool.go new file mode 100644 index 0000000..759de9e --- /dev/null +++ b/internal/mcp/handlers_tool.go @@ -0,0 +1,335 @@ +// Summary: MCP tool-related handler methods for listing tools, calling tools, +// and JSON Schema definitions for tool inputs. +package mcp + +import ( + "encoding/json" + "fmt" +) + +// handleToolsList processes the tools/list request. +// Returns available tools for prompt management operations. +func (s *Server) handleToolsList(req Request) { + s.mu.RLock() + if !s.initialized { + s.mu.RUnlock() + s.sendError(req.ID, ErrCodeInvalidRequest, "Server not initialized") + return + } + s.mu.RUnlock() + + // Define 3 tools for prompt management + tools := []Tool{ + s.makeCreatePromptTool(), + s.makeUpdatePromptTool(), + s.makeDeletePromptTool(), + } + + result := ListToolsResult{ + Tools: tools, + NextCursor: "", // No pagination needed for 3 tools + } + + s.sendResponse(req.ID, result) +} + +// makeCreatePromptTool returns the JSON Schema definition for create_prompt tool. +func (s *Server) makeCreatePromptTool() Tool { + return Tool{ + Name: "create_prompt", + Description: "Creates a new custom prompt template that can be invoked via the MCP prompts capability. The prompt is saved to user.jsonl and becomes immediately available for use.", + InputSchema: s.buildCreatePromptSchema(), + } +} + +// buildCreatePromptSchema constructs the JSON Schema for create_prompt tool inputs. +func (s *Server) buildCreatePromptSchema() map[string]interface{} { + return map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "name": map[string]interface{}{ + "type": "string", + "description": "Unique identifier for the prompt (lowercase, underscores allowed)", + }, + "title": map[string]interface{}{ + "type": "string", + "description": "Human-readable display name for the prompt", + }, + "description": map[string]interface{}{ + "type": "string", + "description": "Detailed explanation of what the prompt does", + }, + "arguments": buildArgumentArraySchema(), + "messages": buildMessageArraySchema(), + "tags": map[string]interface{}{ + "type": "array", + "description": "Optional tags for categorizing the prompt", + "items": map[string]interface{}{"type": "string"}, + }, + }, + "required": []string{"name", "title", "messages"}, + } +} + +// makeUpdatePromptTool returns the JSON Schema definition for update_prompt tool. +func (s *Server) makeUpdatePromptTool() Tool { + return Tool{ + Name: "update_prompt", + Description: "Updates an existing custom prompt template. Only custom prompts (stored in user.jsonl) can be updated; built-in prompts cannot be modified.", + InputSchema: s.buildUpdatePromptSchema(), + } +} + +// buildUpdatePromptSchema constructs the JSON Schema for update_prompt tool inputs. +func (s *Server) buildUpdatePromptSchema() map[string]interface{} { + return map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "name": map[string]interface{}{ + "type": "string", + "description": "Name of the existing prompt to update", + }, + "title": map[string]interface{}{ + "type": "string", + "description": "New display name (optional, only if changing)", + }, + "description": map[string]interface{}{ + "type": "string", + "description": "New description (optional, only if changing)", + }, + "arguments": buildArgumentArraySchema(), + "messages": buildMessageArraySchema(), + "tags": map[string]interface{}{ + "type": "array", + "description": "New tags array (optional, replaces existing if provided)", + "items": map[string]interface{}{"type": "string"}, + }, + }, + "required": []string{"name"}, + } +} + +// makeDeletePromptTool returns the JSON Schema definition for delete_prompt tool. +func (s *Server) makeDeletePromptTool() Tool { + return Tool{ + Name: "delete_prompt", + Description: "Deletes a custom prompt template. Only custom prompts (stored in user.jsonl) can be deleted; built-in prompts cannot be removed.", + InputSchema: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "name": map[string]interface{}{ + "type": "string", + "description": "Name of the prompt to delete", + }, + }, + "required": []string{"name"}, + }, + } +} + +// handleToolsCall processes the tools/call request. +// Dispatches to appropriate tool wrapper based on tool name. +func (s *Server) handleToolsCall(req Request) { + s.mu.RLock() + if !s.initialized { + s.mu.RUnlock() + s.sendError(req.ID, ErrCodeInvalidRequest, "Server not initialized") + return + } + s.mu.RUnlock() + + var params CallToolRequest + if err := json.Unmarshal(req.Params, ¶ms); err != nil { + s.sendError(req.ID, ErrCodeInvalidParams, "Invalid tools/call params") + return + } + + // Dispatch to appropriate tool wrapper + switch params.Name { + case "create_prompt": + s.callCreatePromptTool(req.ID, params.Arguments) + case "update_prompt": + s.callUpdatePromptTool(req.ID, params.Arguments) + case "delete_prompt": + s.callDeletePromptTool(req.ID, params.Arguments) + default: + s.sendToolError(req.ID, fmt.Sprintf("Unknown tool: %s", params.Name)) + } +} + +// callCreatePromptTool executes the create_prompt tool. +// Converts map arguments to CreatePromptRequest and delegates to store.Create. +func (s *Server) callCreatePromptTool(id any, args map[string]interface{}) { + // Convert map to CreatePromptRequest + params, err := convertToCreatePromptRequest(args) + if err != nil { + s.sendToolError(id, fmt.Sprintf("Invalid arguments: %v", err)) + return + } + + // Validate required fields + if err := validateCreateParams(params); err != nil { + s.sendToolError(id, err.Error()) + return + } + + // Build prompt and create + prompt := buildPromptFromCreateParams(params) + if err := s.store.Create(prompt); err != nil { + s.logger.Printf("create prompt error: %v", err) + s.sendToolError(id, fmt.Sprintf("Failed to create prompt: %v", err)) + return + } + + s.logger.Printf("created prompt via tool: %s", params.Name) + + // Sync to slash commands if enabled + if s.syncer != nil { + if err := s.syncer.SyncCreate(prompt); err != nil { + s.logger.Printf("slash command sync failed: %v", err) + } + } + + s.sendToolSuccess(id, fmt.Sprintf("Successfully created prompt: %s", params.Name)) + + // Notify clients that the prompt list has changed + s.sendPromptsListChangedNotification() +} + +// callUpdatePromptTool executes the update_prompt tool. +// Converts map arguments to UpdatePromptRequest and delegates to store.Update. +func (s *Server) callUpdatePromptTool(id any, args map[string]interface{}) { + // Convert map to UpdatePromptRequest + params, err := convertToUpdatePromptRequest(args) + if err != nil { + s.sendToolError(id, fmt.Sprintf("Invalid arguments: %v", err)) + return + } + + if params.Name == "" { + s.sendToolError(id, "Prompt name is required") + return + } + + // Get existing prompt + existing, err := s.store.Get(params.Name) + if err != nil { + s.logger.Printf("get prompt error: %v", err) + s.sendToolError(id, fmt.Sprintf("Prompt not found: %s", params.Name)) + return + } + + // Apply updates + applyPromptUpdates(existing, params) + + if err := s.store.Update(existing); err != nil { + s.logger.Printf("update prompt error: %v", err) + s.sendToolError(id, fmt.Sprintf("Failed to update prompt: %v", err)) + return + } + + s.logger.Printf("updated prompt via tool: %s", params.Name) + + // Sync to slash commands if enabled + if s.syncer != nil { + if err := s.syncer.SyncUpdate(existing); err != nil { + s.logger.Printf("slash command sync failed: %v", err) + } + } + + s.sendToolSuccess(id, fmt.Sprintf("Successfully updated prompt: %s", params.Name)) + + // Notify clients that the prompt list has changed + s.sendPromptsListChangedNotification() +} + +// callDeletePromptTool executes the delete_prompt tool. +// Extracts name from arguments and delegates to store.Delete. +func (s *Server) callDeletePromptTool(id any, args map[string]interface{}) { + // Extract name from arguments + name, ok := args["name"].(string) + if !ok || name == "" { + s.sendToolError(id, "Prompt name is required") + return + } + + if err := s.store.Delete(name); err != nil { + s.logger.Printf("delete prompt error: %v", err) + s.sendToolError(id, fmt.Sprintf("Failed to delete prompt: %v", err)) + return + } + + s.logger.Printf("deleted prompt via tool: %s", name) + + // Delete slash command file if enabled + if s.syncer != nil { + if err := s.syncer.Delete(name); err != nil { + s.logger.Printf("slash command sync delete failed: %v", err) + } + } + + s.sendToolSuccess(id, fmt.Sprintf("Successfully deleted prompt: %s", name)) + + // Notify clients that the prompt list has changed + s.sendPromptsListChangedNotification() +} + +// buildArgumentArraySchema creates the JSON Schema for prompt arguments array. +func buildArgumentArraySchema() map[string]interface{} { + return map[string]interface{}{ + "type": "array", + "description": "Template variables that can be substituted when invoking the prompt", + "items": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "name": map[string]interface{}{ + "type": "string", + "description": "Argument name (used in {{name}} placeholders)", + }, + "description": map[string]interface{}{ + "type": "string", + "description": "Description of the argument's purpose", + }, + "required": map[string]interface{}{ + "type": "boolean", + "description": "Whether this argument must be provided", + }, + }, + "required": []string{"name"}, + }, + } +} + +// buildMessageArraySchema creates the JSON Schema for prompt messages array. +func buildMessageArraySchema() map[string]interface{} { + return map[string]interface{}{ + "type": "array", + "description": "Array of message objects (role + content) that form the prompt", + "items": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "role": map[string]interface{}{ + "type": "string", + "description": "Message role: 'user' or 'assistant'", + "enum": []string{"user", "assistant"}, + }, + "content": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "type": map[string]interface{}{ + "type": "string", + "description": "Content type (currently only 'text' is supported)", + "enum": []string{"text"}, + }, + "text": map[string]interface{}{ + "type": "string", + "description": "The message text (can include {{arg}} placeholders)", + }, + }, + "required": []string{"type", "text"}, + }, + }, + "required": []string{"role", "content"}, + }, + } +} diff --git a/internal/mcp/server.go b/internal/mcp/server.go index 772d311..99cd456 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -1,4 +1,4 @@ -// Summary: MCP server over stdio; manages prompt store, dispatches requests, and handles protocol. +// Summary: MCP server core — struct definition, constructor, main loop, and message dispatch. package mcp import ( @@ -8,9 +8,7 @@ import ( "fmt" "io" "log" - "strings" "sync" - "time" "codeberg.org/snonux/hexai/internal" "codeberg.org/snonux/hexai/internal/promptstore" @@ -50,7 +48,7 @@ func NewServer(r io.Reader, w io.Writer, logger *log.Logger, store promptstore.P syncer: syncer, } - // Initialize dispatch table + // Initialize dispatch table mapping JSON-RPC methods to handler functions s.handlers = map[string]func(Request){ "initialize": s.handleInitialize, "initialized": s.handleInitialized, @@ -166,139 +164,6 @@ func (s *Server) handleInitialized(_ Request) { // No response required for notifications } -// handlePromptsList processes the prompts/list request. -func (s *Server) handlePromptsList(req Request) { - s.mu.RLock() - if !s.initialized { - s.mu.RUnlock() - s.sendError(req.ID, ErrCodeInvalidRequest, "Server not initialized") - return - } - s.mu.RUnlock() - - var params ListPromptsRequest - if req.Params != nil && len(req.Params) > 0 { - if err := json.Unmarshal(req.Params, ¶ms); err != nil { - s.sendError(req.ID, ErrCodeInvalidParams, "Invalid prompts/list params") - return - } - } - - // List prompts from store - prompts, nextCursor, err := s.store.List(params.Cursor, 100) - if err != nil { - s.logger.Printf("store list error: %v", err) - s.sendError(req.ID, ErrCodeInternalError, "Failed to list prompts") - return - } - - // Convert to PromptInfo (initialize as empty slice so JSON marshals as [] not null) - infos := make([]PromptInfo, 0, len(prompts)) - for _, p := range prompts { - args := make([]PromptArgument, len(p.Arguments)) - for i, a := range p.Arguments { - args[i] = PromptArgument{ - Name: a.Name, - Description: a.Description, - Required: a.Required, - } - } - infos = append(infos, PromptInfo{ - Name: p.Name, - Title: p.Title, - Description: p.Description, - Arguments: args, - }) - } - - result := ListPromptsResult{ - Prompts: infos, - NextCursor: nextCursor, - } - - s.sendResponse(req.ID, result) -} - -// handlePromptsGet processes the prompts/get request. -func (s *Server) handlePromptsGet(req Request) { - s.mu.RLock() - if !s.initialized { - s.mu.RUnlock() - s.sendError(req.ID, ErrCodeInvalidRequest, "Server not initialized") - return - } - s.mu.RUnlock() - - var params GetPromptRequest - if err := json.Unmarshal(req.Params, ¶ms); err != nil { - s.sendError(req.ID, ErrCodeInvalidParams, "Invalid prompts/get params") - return - } - - if params.Name == "" { - s.sendError(req.ID, ErrCodeInvalidParams, "Missing prompt name") - return - } - - // Get prompt from store - prompt, err := s.store.Get(params.Name) - if err != nil { - s.logger.Printf("store get error: %v", err) - s.sendError(req.ID, ErrCodeInvalidParams, fmt.Sprintf("Prompt not found: %s", params.Name)) - return - } - - // Render prompt with arguments - messages, err := s.renderPrompt(prompt, params.Arguments) - if err != nil { - s.logger.Printf("render error: %v", err) - s.sendError(req.ID, ErrCodeInvalidParams, err.Error()) - return - } - - result := GetPromptResult{ - Description: prompt.Description, - Messages: messages, - } - - s.sendResponse(req.ID, result) -} - -// renderPrompt substitutes template arguments in prompt messages. -// Returns error if required arguments are missing. -func (s *Server) renderPrompt(prompt *promptstore.Prompt, args map[string]string) ([]PromptMessage, error) { - // Validate required arguments - for _, arg := range prompt.Arguments { - if arg.Required { - if _, ok := args[arg.Name]; !ok { - return nil, fmt.Errorf("missing required argument: %s", arg.Name) - } - } - } - - // Render each message - var rendered []PromptMessage - for _, msg := range prompt.Messages { - text := msg.Content.Text - - // Simple template substitution: {{arg}} -> value - for key, val := range args { - placeholder := "{{" + key + "}}" - text = strings.ReplaceAll(text, placeholder, val) - } - - rendered = append(rendered, PromptMessage{ - Role: msg.Role, - Content: MessageContent{ - Type: msg.Content.Type, - Text: text, - }, - }) - } - - return rendered, nil -} - // sendResponse sends a successful JSON-RPC response. func (s *Server) sendResponse(id any, result any) { resp := Response{ @@ -326,518 +191,6 @@ func (s *Server) sendError(id any, code int, message string) { } } -// handlePromptsCreate processes the prompts/create request. -// Creates a new custom prompt in user.jsonl. -func (s *Server) handlePromptsCreate(req Request) { - s.mu.RLock() - if !s.initialized { - s.mu.RUnlock() - s.sendError(req.ID, ErrCodeInvalidRequest, "Server not initialized") - return - } - s.mu.RUnlock() - - var params CreatePromptRequest - if err := json.Unmarshal(req.Params, ¶ms); err != nil { - s.sendError(req.ID, ErrCodeInvalidParams, "Invalid prompts/create params") - return - } - - // Validate required fields - if err := validateCreateParams(params); err != nil { - s.sendError(req.ID, ErrCodeInvalidParams, err.Error()) - return - } - - // Build prompt from params - prompt := buildPromptFromCreateParams(params) - - if err := s.store.Create(prompt); err != nil { - s.logger.Printf("create prompt error: %v", err) - s.sendError(req.ID, ErrCodeInternalError, fmt.Sprintf("Failed to create prompt: %v", err)) - return - } - - result := PromptOperationResult{ - Success: true, - Message: fmt.Sprintf("Created prompt: %s", params.Name), - } - - s.logger.Printf("created prompt: %s", params.Name) - - // Sync to slash commands if enabled - if s.syncer != nil { - if err := s.syncer.SyncCreate(prompt); err != nil { - s.logger.Printf("slash command sync failed: %v", err) - } - } - - s.sendResponse(req.ID, result) - - // Notify clients that the prompt list has changed - s.sendPromptsListChangedNotification() -} - -// validateCreateParams validates required fields for prompt creation. -func validateCreateParams(params CreatePromptRequest) error { - if params.Name == "" { - return fmt.Errorf("prompt name is required") - } - if params.Title == "" { - return fmt.Errorf("prompt title is required") - } - if len(params.Messages) == 0 { - return fmt.Errorf("at least one message is required") - } - return nil -} - -// buildPromptFromCreateParams converts CreatePromptRequest to Prompt. -func buildPromptFromCreateParams(params CreatePromptRequest) *promptstore.Prompt { - prompt := &promptstore.Prompt{ - Name: params.Name, - Title: params.Title, - Description: params.Description, - Tags: params.Tags, - Created: time.Now(), - Updated: time.Now(), - } - - // Convert arguments - for _, arg := range params.Arguments { - prompt.Arguments = append(prompt.Arguments, promptstore.PromptArgument{ - Name: arg.Name, - Description: arg.Description, - Required: arg.Required, - }) - } - - // Convert messages - for _, msg := range params.Messages { - prompt.Messages = append(prompt.Messages, promptstore.PromptMessage{ - Role: msg.Role, - Content: promptstore.MessageContent{ - Type: msg.Content.Type, - Text: msg.Content.Text, - }, - }) - } - - return prompt -} - -// handlePromptsUpdate processes the prompts/update request. -// Updates an existing custom prompt in user.jsonl. -func (s *Server) handlePromptsUpdate(req Request) { - s.mu.RLock() - if !s.initialized { - s.mu.RUnlock() - s.sendError(req.ID, ErrCodeInvalidRequest, "Server not initialized") - return - } - s.mu.RUnlock() - - var params UpdatePromptRequest - if err := json.Unmarshal(req.Params, ¶ms); err != nil { - s.sendError(req.ID, ErrCodeInvalidParams, "Invalid prompts/update params") - return - } - - if params.Name == "" { - s.sendError(req.ID, ErrCodeInvalidParams, "Prompt name is required") - return - } - - // Get existing prompt - existing, err := s.store.Get(params.Name) - if err != nil { - s.logger.Printf("get prompt error: %v", err) - s.sendError(req.ID, ErrCodeInvalidParams, fmt.Sprintf("Prompt not found: %s", params.Name)) - return - } - - // Apply updates to existing prompt - applyPromptUpdates(existing, params) - - if err := s.store.Update(existing); err != nil { - s.logger.Printf("update prompt error: %v", err) - s.sendError(req.ID, ErrCodeInternalError, fmt.Sprintf("Failed to update prompt: %v", err)) - return - } - - result := PromptOperationResult{ - Success: true, - Message: fmt.Sprintf("Updated prompt: %s", params.Name), - } - - s.logger.Printf("updated prompt: %s", params.Name) - - // Sync to slash commands if enabled - if s.syncer != nil { - if err := s.syncer.SyncUpdate(existing); err != nil { - s.logger.Printf("slash command sync failed: %v", err) - } - } - - s.sendResponse(req.ID, result) - - // Notify clients that the prompt list has changed - s.sendPromptsListChangedNotification() -} - -// applyPromptUpdates applies update parameters to an existing prompt. -func applyPromptUpdates(existing *promptstore.Prompt, params UpdatePromptRequest) { - // Update fields (only if provided) - if params.Title != "" { - existing.Title = params.Title - } - if params.Description != "" { - existing.Description = params.Description - } - if len(params.Arguments) > 0 { - existing.Arguments = nil - for _, arg := range params.Arguments { - existing.Arguments = append(existing.Arguments, promptstore.PromptArgument{ - Name: arg.Name, - Description: arg.Description, - Required: arg.Required, - }) - } - } - if len(params.Messages) > 0 { - existing.Messages = nil - for _, msg := range params.Messages { - existing.Messages = append(existing.Messages, promptstore.PromptMessage{ - Role: msg.Role, - Content: promptstore.MessageContent{ - Type: msg.Content.Type, - Text: msg.Content.Text, - }, - }) - } - } - if len(params.Tags) > 0 { - existing.Tags = params.Tags - } - - existing.Updated = time.Now() -} - -// handlePromptsDelete processes the prompts/delete request. -// Deletes a custom prompt from user.jsonl. -func (s *Server) handlePromptsDelete(req Request) { - s.mu.RLock() - if !s.initialized { - s.mu.RUnlock() - s.sendError(req.ID, ErrCodeInvalidRequest, "Server not initialized") - return - } - s.mu.RUnlock() - - var params DeletePromptRequest - if err := json.Unmarshal(req.Params, ¶ms); err != nil { - s.sendError(req.ID, ErrCodeInvalidParams, "Invalid prompts/delete params") - return - } - - if params.Name == "" { - s.sendError(req.ID, ErrCodeInvalidParams, "Prompt name is required") - return - } - - if err := s.store.Delete(params.Name); err != nil { - s.logger.Printf("delete prompt error: %v", err) - s.sendError(req.ID, ErrCodeInternalError, fmt.Sprintf("Failed to delete prompt: %v", err)) - return - } - - result := PromptOperationResult{ - Success: true, - Message: fmt.Sprintf("Deleted prompt: %s", params.Name), - } - - s.logger.Printf("deleted prompt: %s", params.Name) - - // Delete slash command file if enabled - if s.syncer != nil { - if err := s.syncer.Delete(params.Name); err != nil { - s.logger.Printf("slash command sync delete failed: %v", err) - } - } - - s.sendResponse(req.ID, result) - - // Notify clients that the prompt list has changed - s.sendPromptsListChangedNotification() -} - -// handleToolsList processes the tools/list request. -// Returns available tools for prompt management operations. -func (s *Server) handleToolsList(req Request) { - s.mu.RLock() - if !s.initialized { - s.mu.RUnlock() - s.sendError(req.ID, ErrCodeInvalidRequest, "Server not initialized") - return - } - s.mu.RUnlock() - - // Define 3 tools for prompt management - tools := []Tool{ - s.makeCreatePromptTool(), - s.makeUpdatePromptTool(), - s.makeDeletePromptTool(), - } - - result := ListToolsResult{ - Tools: tools, - NextCursor: "", // No pagination needed for 3 tools - } - - s.sendResponse(req.ID, result) -} - -// makeCreatePromptTool returns the JSON Schema definition for create_prompt tool. -func (s *Server) makeCreatePromptTool() Tool { - return Tool{ - Name: "create_prompt", - Description: "Creates a new custom prompt template that can be invoked via the MCP prompts capability. The prompt is saved to user.jsonl and becomes immediately available for use.", - InputSchema: s.buildCreatePromptSchema(), - } -} - -// buildCreatePromptSchema constructs the JSON Schema for create_prompt tool inputs. -func (s *Server) buildCreatePromptSchema() map[string]interface{} { - return map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "name": map[string]interface{}{ - "type": "string", - "description": "Unique identifier for the prompt (lowercase, underscores allowed)", - }, - "title": map[string]interface{}{ - "type": "string", - "description": "Human-readable display name for the prompt", - }, - "description": map[string]interface{}{ - "type": "string", - "description": "Detailed explanation of what the prompt does", - }, - "arguments": buildArgumentArraySchema(), - "messages": buildMessageArraySchema(), - "tags": map[string]interface{}{ - "type": "array", - "description": "Optional tags for categorizing the prompt", - "items": map[string]interface{}{"type": "string"}, - }, - }, - "required": []string{"name", "title", "messages"}, - } -} - -// makeUpdatePromptTool returns the JSON Schema definition for update_prompt tool. -func (s *Server) makeUpdatePromptTool() Tool { - return Tool{ - Name: "update_prompt", - Description: "Updates an existing custom prompt template. Only custom prompts (stored in user.jsonl) can be updated; built-in prompts cannot be modified.", - InputSchema: s.buildUpdatePromptSchema(), - } -} - -// buildUpdatePromptSchema constructs the JSON Schema for update_prompt tool inputs. -func (s *Server) buildUpdatePromptSchema() map[string]interface{} { - return map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "name": map[string]interface{}{ - "type": "string", - "description": "Name of the existing prompt to update", - }, - "title": map[string]interface{}{ - "type": "string", - "description": "New display name (optional, only if changing)", - }, - "description": map[string]interface{}{ - "type": "string", - "description": "New description (optional, only if changing)", - }, - "arguments": buildArgumentArraySchema(), - "messages": buildMessageArraySchema(), - "tags": map[string]interface{}{ - "type": "array", - "description": "New tags array (optional, replaces existing if provided)", - "items": map[string]interface{}{"type": "string"}, - }, - }, - "required": []string{"name"}, - } -} - -// makeDeletePromptTool returns the JSON Schema definition for delete_prompt tool. -func (s *Server) makeDeletePromptTool() Tool { - return Tool{ - Name: "delete_prompt", - Description: "Deletes a custom prompt template. Only custom prompts (stored in user.jsonl) can be deleted; built-in prompts cannot be removed.", - InputSchema: map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "name": map[string]interface{}{ - "type": "string", - "description": "Name of the prompt to delete", - }, - }, - "required": []string{"name"}, - }, - } -} - -// handleToolsCall processes the tools/call request. -// Dispatches to appropriate tool wrapper based on tool name. -func (s *Server) handleToolsCall(req Request) { - s.mu.RLock() - if !s.initialized { - s.mu.RUnlock() - s.sendError(req.ID, ErrCodeInvalidRequest, "Server not initialized") - return - } - s.mu.RUnlock() - - var params CallToolRequest - if err := json.Unmarshal(req.Params, ¶ms); err != nil { - s.sendError(req.ID, ErrCodeInvalidParams, "Invalid tools/call params") - return - } - - // Dispatch to appropriate tool wrapper - switch params.Name { - case "create_prompt": - s.callCreatePromptTool(req.ID, params.Arguments) - case "update_prompt": - s.callUpdatePromptTool(req.ID, params.Arguments) - case "delete_prompt": - s.callDeletePromptTool(req.ID, params.Arguments) - default: - s.sendToolError(req.ID, fmt.Sprintf("Unknown tool: %s", params.Name)) - } -} - -// callCreatePromptTool executes the create_prompt tool. -// Converts map arguments to CreatePromptRequest and delegates to store.Create. -func (s *Server) callCreatePromptTool(id any, args map[string]interface{}) { - // Convert map to CreatePromptRequest - params, err := convertToCreatePromptRequest(args) - if err != nil { - s.sendToolError(id, fmt.Sprintf("Invalid arguments: %v", err)) - return - } - - // Validate required fields - if err := validateCreateParams(params); err != nil { - s.sendToolError(id, err.Error()) - return - } - - // Build prompt and create - prompt := buildPromptFromCreateParams(params) - if err := s.store.Create(prompt); err != nil { - s.logger.Printf("create prompt error: %v", err) - s.sendToolError(id, fmt.Sprintf("Failed to create prompt: %v", err)) - return - } - - s.logger.Printf("created prompt via tool: %s", params.Name) - - // Sync to slash commands if enabled - if s.syncer != nil { - if err := s.syncer.SyncCreate(prompt); err != nil { - s.logger.Printf("slash command sync failed: %v", err) - } - } - - s.sendToolSuccess(id, fmt.Sprintf("Successfully created prompt: %s", params.Name)) - - // Notify clients that the prompt list has changed - s.sendPromptsListChangedNotification() -} - -// callUpdatePromptTool executes the update_prompt tool. -// Converts map arguments to UpdatePromptRequest and delegates to store.Update. -func (s *Server) callUpdatePromptTool(id any, args map[string]interface{}) { - // Convert map to UpdatePromptRequest - params, err := convertToUpdatePromptRequest(args) - if err != nil { - s.sendToolError(id, fmt.Sprintf("Invalid arguments: %v", err)) - return - } - - if params.Name == "" { - s.sendToolError(id, "Prompt name is required") - return - } - - // Get existing prompt - existing, err := s.store.Get(params.Name) - if err != nil { - s.logger.Printf("get prompt error: %v", err) - s.sendToolError(id, fmt.Sprintf("Prompt not found: %s", params.Name)) - return - } - - // Apply updates - applyPromptUpdates(existing, params) - - if err := s.store.Update(existing); err != nil { - s.logger.Printf("update prompt error: %v", err) - s.sendToolError(id, fmt.Sprintf("Failed to update prompt: %v", err)) - return - } - - s.logger.Printf("updated prompt via tool: %s", params.Name) - - // Sync to slash commands if enabled - if s.syncer != nil { - if err := s.syncer.SyncUpdate(existing); err != nil { - s.logger.Printf("slash command sync failed: %v", err) - } - } - - s.sendToolSuccess(id, fmt.Sprintf("Successfully updated prompt: %s", params.Name)) - - // Notify clients that the prompt list has changed - s.sendPromptsListChangedNotification() -} - -// callDeletePromptTool executes the delete_prompt tool. -// Extracts name from arguments and delegates to store.Delete. -func (s *Server) callDeletePromptTool(id any, args map[string]interface{}) { - // Extract name from arguments - name, ok := args["name"].(string) - if !ok || name == "" { - s.sendToolError(id, "Prompt name is required") - return - } - - if err := s.store.Delete(name); err != nil { - s.logger.Printf("delete prompt error: %v", err) - s.sendToolError(id, fmt.Sprintf("Failed to delete prompt: %v", err)) - return - } - - s.logger.Printf("deleted prompt via tool: %s", name) - - // Delete slash command file if enabled - if s.syncer != nil { - if err := s.syncer.Delete(name); err != nil { - s.logger.Printf("slash command sync delete failed: %v", err) - } - } - - s.sendToolSuccess(id, fmt.Sprintf("Successfully deleted prompt: %s", name)) - - // Notify clients that the prompt list has changed - s.sendPromptsListChangedNotification() -} - // sendToolSuccess sends a successful tool result. func (s *Server) sendToolSuccess(id any, message string) { result := CallToolResult{ @@ -856,98 +209,6 @@ func (s *Server) sendToolError(id any, message string) { s.sendResponse(id, result) } -// convertToCreatePromptRequest converts map arguments to CreatePromptRequest. -func convertToCreatePromptRequest(args map[string]interface{}) (CreatePromptRequest, error) { - // Marshal to JSON then unmarshal to struct for type conversion - data, err := json.Marshal(args) - if err != nil { - return CreatePromptRequest{}, fmt.Errorf("marshal args: %w", err) - } - - var req CreatePromptRequest - if err := json.Unmarshal(data, &req); err != nil { - return CreatePromptRequest{}, fmt.Errorf("unmarshal to CreatePromptRequest: %w", err) - } - - return req, nil -} - -// convertToUpdatePromptRequest converts map arguments to UpdatePromptRequest. -func convertToUpdatePromptRequest(args map[string]interface{}) (UpdatePromptRequest, error) { - // Marshal to JSON then unmarshal to struct for type conversion - data, err := json.Marshal(args) - if err != nil { - return UpdatePromptRequest{}, fmt.Errorf("marshal args: %w", err) - } - - var req UpdatePromptRequest - if err := json.Unmarshal(data, &req); err != nil { - return UpdatePromptRequest{}, fmt.Errorf("unmarshal to UpdatePromptRequest: %w", err) - } - - return req, nil -} - -// buildArgumentArraySchema creates the JSON Schema for prompt arguments array. -func buildArgumentArraySchema() map[string]interface{} { - return map[string]interface{}{ - "type": "array", - "description": "Template variables that can be substituted when invoking the prompt", - "items": map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "name": map[string]interface{}{ - "type": "string", - "description": "Argument name (used in {{name}} placeholders)", - }, - "description": map[string]interface{}{ - "type": "string", - "description": "Description of the argument's purpose", - }, - "required": map[string]interface{}{ - "type": "boolean", - "description": "Whether this argument must be provided", - }, - }, - "required": []string{"name"}, - }, - } -} - -// buildMessageArraySchema creates the JSON Schema for prompt messages array. -func buildMessageArraySchema() map[string]interface{} { - return map[string]interface{}{ - "type": "array", - "description": "Array of message objects (role + content) that form the prompt", - "items": map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "role": map[string]interface{}{ - "type": "string", - "description": "Message role: 'user' or 'assistant'", - "enum": []string{"user", "assistant"}, - }, - "content": map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "type": map[string]interface{}{ - "type": "string", - "description": "Content type (currently only 'text' is supported)", - "enum": []string{"text"}, - }, - "text": map[string]interface{}{ - "type": "string", - "description": "The message text (can include {{arg}} placeholders)", - }, - }, - "required": []string{"type", "text"}, - }, - }, - "required": []string{"role", "content"}, - }, - } -} - // sendPromptsListChangedNotification notifies the client that the prompt list has changed. // This allows clients to refresh their cached prompt lists. func (s *Server) sendPromptsListChangedNotification() { |
