summaryrefslogtreecommitdiff
path: root/internal/mcp/server.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/mcp/server.go')
-rw-r--r--internal/mcp/server.go382
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, &params); 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)
+ }
}