diff options
Diffstat (limited to 'internal/mcp/server.go')
| -rw-r--r-- | internal/mcp/server.go | 382 |
1 files changed, 381 insertions, 1 deletions
diff --git a/internal/mcp/server.go b/internal/mcp/server.go index 6ac7537..58de01d 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -49,6 +49,8 @@ func NewServer(r io.Reader, w io.Writer, logger *log.Logger, store promptstore.P "prompts/create": s.handlePromptsCreate, "prompts/update": s.handlePromptsUpdate, "prompts/delete": s.handlePromptsDelete, + "tools/list": s.handleToolsList, + "tools/call": s.handleToolsCall, "notifications/initialized": s.handleInitialized, } @@ -116,9 +118,12 @@ func (s *Server) handleInitialize(req Request) { ProtocolVersion: negotiatedVersion, Capabilities: ServerCapabilities{ Prompts: &PromptsCapability{ - ListChanged: false, + ListChanged: true, // Server sends notifications when prompt list changes Mutable: true, // Advertise that we support create/update/delete }, + Tools: &ToolsCapability{ + ListChanged: false, // Tool list is static (no dynamic changes) + }, }, ServerInfo: ServerInfo{ Name: "hexai-mcp-server", @@ -350,6 +355,9 @@ func (s *Server) handlePromptsCreate(req Request) { s.logger.Printf("created prompt: %s", params.Name) s.sendResponse(req.ID, result) + + // Notify clients that the prompt list has changed + s.sendPromptsListChangedNotification() } // validateCreateParams validates required fields for prompt creation. @@ -446,6 +454,9 @@ func (s *Server) handlePromptsUpdate(req Request) { s.logger.Printf("updated prompt: %s", params.Name) s.sendResponse(req.ID, result) + + // Notify clients that the prompt list has changed + s.sendPromptsListChangedNotification() } // applyPromptUpdates applies update parameters to an existing prompt. @@ -521,4 +532,373 @@ func (s *Server) handlePromptsDelete(req Request) { s.logger.Printf("deleted prompt: %s", params.Name) 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) + 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) + 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) + 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{ + Content: []ToolContent{{Type: "text", Text: message}}, + IsError: false, + } + s.sendResponse(id, result) +} + +// sendToolError sends a tool error result (business logic error, not protocol error). +func (s *Server) sendToolError(id any, message string) { + result := CallToolResult{ + Content: []ToolContent{{Type: "text", Text: message}}, + IsError: true, + } + 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() { + notification := map[string]interface{}{ + "jsonrpc": "2.0", + "method": "notifications/prompts/list_changed", + } + + if err := s.writeMessage(notification); err != nil { + s.logger.Printf("failed to send prompts/list_changed notification: %v", err) + } } |
