diff options
| author | Paul Buetow <paul@buetow.org> | 2026-02-10 19:28:27 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-02-10 19:28:27 +0200 |
| commit | 5551695f3b0d10c9a22cfacdb10c2cf7bd572421 (patch) | |
| tree | 282611eacf1fd4c38d54d5cea87decdf2b1cbdb7 | |
| parent | ec745129258ae800065e302a2a40b54488cbca08 (diff) | |
Add MCP server implementation with comprehensive test coverage
Implements a full Model Context Protocol (MCP) server for managing and serving prompts
to LLM applications. The server provides CRUD operations for prompts with automatic
backups and template rendering support.
Key additions:
- cmd/hexai-mcp-server: Main MCP server binary entrypoint
- internal/hexaimcp: Server orchestrator with configuration and setup
- internal/mcp: Core MCP protocol implementation (JSON-RPC 2.0)
- internal/promptstore: Prompt storage with JSONL backend and automatic backups
- Comprehensive test suites achieving 80%+ coverage for all MCP packages
- Magefile targets for building and installing the MCP server
- Complete documentation for setup, API, prompts, and backups
Test coverage:
- internal/hexaimcp: 84.3%
- internal/mcp: 80.3%
- internal/promptstore: 81.2%
- Overall project: 81.5%
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
| -rw-r--r-- | MCP-SERVER-PLAN.md | 1407 | ||||
| -rw-r--r-- | Magefile.go | 20 | ||||
| -rw-r--r-- | README.md | 12 | ||||
| -rw-r--r-- | cmd/hexai-mcp-server/main.go | 46 | ||||
| -rw-r--r-- | docs/mcp-api.md | 459 | ||||
| -rw-r--r-- | docs/mcp-automatic-backups.md | 198 | ||||
| -rw-r--r-- | docs/mcp-features-summary.md | 336 | ||||
| -rw-r--r-- | docs/mcp-managing-prompts.md | 324 | ||||
| -rw-r--r-- | docs/mcp-prompts.md | 515 | ||||
| -rw-r--r-- | docs/mcp-server-complete.md | 292 | ||||
| -rw-r--r-- | docs/mcp-setup.md | 282 | ||||
| -rw-r--r-- | internal/appconfig/config.go | 20 | ||||
| -rw-r--r-- | internal/hexaimcp/run.go | 158 | ||||
| -rw-r--r-- | internal/hexaimcp/run_test.go | 342 | ||||
| -rw-r--r-- | internal/mcp/handlers_test.go | 955 | ||||
| -rw-r--r-- | internal/mcp/server.go | 494 | ||||
| -rw-r--r-- | internal/mcp/server_test.go | 505 | ||||
| -rw-r--r-- | internal/mcp/transport.go | 69 | ||||
| -rw-r--r-- | internal/mcp/types.go | 187 | ||||
| -rw-r--r-- | internal/promptstore/backup_test.go | 308 | ||||
| -rw-r--r-- | internal/promptstore/builtin.go | 156 | ||||
| -rw-r--r-- | internal/promptstore/store.go | 547 | ||||
| -rw-r--r-- | internal/promptstore/store_test.go | 311 | ||||
| -rw-r--r-- | internal/promptstore/types.go | 39 |
24 files changed, 7978 insertions, 4 deletions
diff --git a/MCP-SERVER-PLAN.md b/MCP-SERVER-PLAN.md new file mode 100644 index 0000000..f6b1f44 --- /dev/null +++ b/MCP-SERVER-PLAN.md @@ -0,0 +1,1407 @@ +# Plan: hexai-mcp-server - MCP Server for Prompts and Runbooks + +## Context + +This change adds a new MCP (Model Context Protocol) server to hexai for managing prompts and runbooks. Currently, hexai has prompts hardcoded in the configuration file, making it difficult to share, version, and dynamically manage reusable prompts. An MCP server provides a standardized protocol for AI agents (like Claude Code CLI, Cursor) to discover and use prompts/runbooks from hexai. + +**Why this is needed:** +- Centralized prompt management: Store, update, search, and retrieve prompts +- Agent-agnostic: Works with any MCP-compatible agent (Claude Code, Cursor, etc.) +- Follows MCP specification: Industry-standard protocol for prompt/tool/resource discovery +- Reuses hexai patterns: Leverages existing LSP server architecture and JSONL storage + +**Intended outcome:** +- New `hexai-mcp-server` binary that implements MCP protocol over stdio +- File-based prompt storage using JSONL format (similar to tmux-edit history) +- Easy installation for agents via config files +- 80%+ unit test coverage with testable architecture + +--- + +## Architecture Overview + +### Command Structure +Following hexai's multi-binary pattern: + +``` +cmd/hexai-mcp-server/ +└── main.go # Flag parsing + delegation (~60 lines) + # Flags: --log, --config, --prompts-dir, --version + +internal/hexaimcp/ +├── run.go # Main orchestrator (~150 lines) +├── run_test.go # Unit tests +└── testhelpers_test.go # Test helpers and mocks + +internal/mcp/ +├── server.go # MCP server loop + dispatch (~400 lines) +├── server_test.go # Server tests +├── transport.go # JSON-RPC transport (~70 lines, reuses LSP pattern) +├── transport_test.go # Transport tests +├── types.go # MCP protocol types (~200 lines) +├── handlers.go # Protocol handlers (~300 lines) +├── handlers_test.go # Handler tests +└── testhelpers_test.go # Test mocks and helpers + +internal/promptstore/ +├── store.go # Prompt storage interface + impl (~200 lines) +├── store_test.go # Store tests (90%+ coverage) +├── types.go # Prompt data models (~100 lines) +├── jsonl.go # JSONL read/write (~150 lines) +├── jsonl_test.go # JSONL tests +└── builtin.go # Built-in prompts (~100 lines) +``` + +--- + +## Storage Design: JSONL-Based + +**Default Storage Location:** +``` +~/.local/share/hexai/prompts/ (XDG_DATA_HOME) +├── default.jsonl # System/built-in prompts +└── user.jsonl # User-created prompts +``` + +**Configurable via:** +1. **Command-line flag**: `--prompts-dir /path/to/prompts` +2. **Config file**: `mcp_prompts_dir = "/path/to/prompts"` in config.toml +3. **Environment variable**: `HEXAI_MCP_PROMPTS_DIR=/path/to/prompts` +4. **Default**: `$XDG_DATA_HOME/hexai/prompts/` or `~/.local/share/hexai/prompts/` + +**Precedence order** (highest to lowest): +1. Command-line flag +2. Environment variable +3. Config file +4. Default XDG location + +**Rationale:** +- Matches existing pattern in `internal/tmuxedit/history.go` +- Human-readable and editable +- Git-friendly for version control +- No database dependencies +- Atomic append operations +- Easy backup and sync +- Allows project-specific prompt collections +- Enables shared/network storage if needed + +**JSONL Format (one prompt per line):** +```json +{"name":"code_review","title":"Request Code Review","description":"Analyzes code quality","arguments":[{"name":"code","description":"Code to review","required":true}],"messages":[{"role":"user","content":{"type":"text","text":"Review: {{code}}"}}],"tags":["development","review"],"created":"2026-02-10T12:00:00Z","updated":"2026-02-10T12:00:00Z"} +``` + +--- + +## Data Model + +### Core Types + +```go +// internal/promptstore/types.go + +// Prompt represents a reusable prompt template with arguments +type Prompt struct { + Name string `json:"name"` // Unique identifier (alphanumeric + underscores) + Title string `json:"title"` // Display name + Description string `json:"description"` // Human-readable description + Arguments []PromptArgument `json:"arguments"` // Template variables + Messages []PromptMessage `json:"messages"` // Conversation messages + Tags []string `json:"tags"` // Categorization tags + Created time.Time `json:"created"` // Creation timestamp + Updated time.Time `json:"updated"` // Last update timestamp +} + +// PromptArgument defines a template variable +type PromptArgument struct { + Name string `json:"name"` // Variable name (used in {{name}}) + Description string `json:"description"` // Human-readable description + Required bool `json:"required"` // Whether argument is required +} + +// PromptMessage represents a conversation message +type PromptMessage struct { + Role string `json:"role"` // "user" or "assistant" + Content MessageContent `json:"content"` // Message content +} + +// MessageContent contains the actual message data +type MessageContent struct { + Type string `json:"type"` // "text", "image", "resource" + Text string `json:"text,omitempty"` +} +``` + +--- + +## MCP Protocol Implementation + +### Transport Layer (Reuses LSP Pattern) + +Based on `internal/lsp/transport.go`: + +```go +// internal/mcp/transport.go + +// readMessage reads a Content-Length framed JSON-RPC message +func (s *Server) readMessage() ([]byte, error) { + // Parse Content-Length header + // Read exact bytes from stream + // Return raw JSON message +} + +// writeMessage writes a JSON-RPC response with Content-Length framing +func (s *Server) writeMessage(v any) { + // Marshal to JSON + // Write "Content-Length: N\r\n\r\n" + // Write JSON body + // Thread-safe with mutex +} +``` + +### MCP Protocol Types + +```go +// internal/mcp/types.go + +// Request represents an MCP JSON-RPC 2.0 request +type Request struct { + JSONRPC string `json:"jsonrpc"` // Always "2.0" + ID any `json:"id"` // Request ID (string or number) + Method string `json:"method"` // Method name + Params json.RawMessage `json:"params,omitempty"` +} + +// Response represents an MCP JSON-RPC 2.0 response +type Response struct { + JSONRPC string `json:"jsonrpc"` // Always "2.0" + ID any `json:"id"` // Matching request ID + Result any `json:"result,omitempty"` + Error *RespError `json:"error,omitempty"` +} + +// InitializeRequest is the first message from client +type InitializeRequest struct { + ProtocolVersion string `json:"protocolVersion"` // "2024-11-05" + Capabilities Capabilities `json:"capabilities"` + ClientInfo ClientInfo `json:"clientInfo"` +} + +// Capabilities describes what the client/server supports +type Capabilities struct { + Prompts *PromptsCapability `json:"prompts,omitempty"` + Resources *ResourcesCapability `json:"resources,omitempty"` + Tools *ToolsCapability `json:"tools,omitempty"` +} + +// ListPromptsResult contains paginated prompts +type ListPromptsResult struct { + Prompts []PromptInfo `json:"prompts"` + NextCursor string `json:"nextCursor,omitempty"` +} + +// GetPromptResult contains a rendered prompt +type GetPromptResult struct { + Description string `json:"description,omitempty"` + Messages []PromptMessage `json:"messages"` +} +``` + +### MCP Method Handlers + +**Required Methods:** +1. `initialize` - Capability negotiation +2. `prompts/list` - List available prompts (with pagination) +3. `prompts/get` - Get specific prompt with arguments rendered +4. `notifications/initialized` - Client ready signal (no response) + +**Handler Dispatch Pattern (from LSP):** +```go +// internal/mcp/server.go + +func (s *Server) setupHandlers() { + s.handlers = map[string]func(Request){ + "initialize": s.handleInitialize, + "prompts/list": s.handlePromptsList, + "prompts/get": s.handlePromptsGet, + "notifications/initialized": s.handleInitialized, + } +} + +func (s *Server) handleInitialize(req Request) { + // Parse InitializeRequest + // Validate protocol version + // Return server capabilities + // Set initialized flag +} + +func (s *Server) handlePromptsList(req Request) { + // Parse pagination params (cursor, limit) + // Call promptStore.List(cursor, limit) + // Return ListPromptsResult with nextCursor +} + +func (s *Server) handlePromptsGet(req Request) { + // Parse GetPromptRequest (name, arguments) + // Call promptStore.Get(name) + // Render template with arguments + // Return GetPromptResult +} +``` + +--- + +## Testability Design + +### Dependency Injection via Interfaces + +```go +// internal/promptstore/store.go + +// PromptStore defines the interface for prompt storage +// This allows easy mocking in tests +type PromptStore interface { + List(cursor string, limit int) ([]Prompt, string, error) + Get(name string) (*Prompt, error) + Create(prompt *Prompt) error + Update(prompt *Prompt) error + Delete(name string) error + Search(tags []string) ([]Prompt, error) +} + +// JSONLStore is the file-based implementation +type JSONLStore struct { + dataDir string + mu sync.RWMutex + // Package variable for file operations (can be mocked) + readFileFn func(string) ([]byte, error) + writeFileFn func(string, []byte, os.FileMode) error +} + +// NewJSONLStore creates a new JSONL-based store +func NewJSONLStore(dataDir string) (PromptStore, error) { + return &JSONLStore{ + dataDir: dataDir, + readFileFn: os.ReadFile, + writeFileFn: os.WriteFile, + }, nil +} +``` + +### Server Factory Pattern (from LSP) + +```go +// internal/hexaimcp/run.go + +// ServerRunner interface for dependency injection +type ServerRunner interface { + Run() error +} + +// ServerFactory creates a server (testable) +type ServerFactory func( + r io.Reader, + w io.Writer, + logger *log.Logger, + store promptstore.PromptStore, +) ServerRunner + +// RunWithFactory allows test injection +func RunWithFactory( + logPath string, + configPath string, + stdin io.Reader, + stdout io.Writer, + stderr io.Writer, + factory ServerFactory, +) error { + // Load config + // Setup logger + // Create prompt store + // Call factory to create server + // Run server +} + +// Run is the main entry point (uses default factory) +func Run(logPath, configPath string, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { + return RunWithFactory(logPath, configPath, stdin, stdout, stderr, defaultServerFactory) +} +``` + +### Mock Implementation for Tests + +```go +// internal/mcp/testhelpers_test.go + +// mockPromptStore implements PromptStore for testing +type mockPromptStore struct { + prompts map[string]*promptstore.Prompt + listFn func(string, int) ([]promptstore.Prompt, string, error) + getFn func(string) (*promptstore.Prompt, error) +} + +func (m *mockPromptStore) List(cursor string, limit int) ([]promptstore.Prompt, string, error) { + if m.listFn != nil { + return m.listFn(cursor, limit) + } + // Default implementation +} + +func (m *mockPromptStore) Get(name string) (*promptstore.Prompt, error) { + if m.getFn != nil { + return m.getFn(name) + } + p, ok := m.prompts[name] + if !ok { + return nil, fmt.Errorf("prompt not found: %s", name) + } + return p, nil +} +``` + +### Table-Driven Tests + +```go +// internal/promptstore/jsonl_test.go + +func TestJSONLStore_Get(t *testing.T) { + tests := []struct { + name string + promptName string + fileData string + wantErr bool + wantPrompt *Prompt + }{ + { + name: "existing prompt", + promptName: "test", + fileData: `{"name":"test","title":"Test","messages":[]}` + "\n", + wantErr: false, + wantPrompt: &Prompt{Name: "test", Title: "Test"}, + }, + { + name: "prompt not found", + promptName: "missing", + fileData: `{"name":"other","title":"Other","messages":[]}` + "\n", + wantErr: true, + wantPrompt: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup + tmpDir := t.TempDir() + setupTestFile(t, tmpDir, tt.fileData) + store, _ := NewJSONLStore(tmpDir) + + // Execute + got, err := store.Get(tt.promptName) + + // Assert + if (err != nil) != tt.wantErr { + t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(got, tt.wantPrompt) { + t.Errorf("Get() = %v, want %v", got, tt.wantPrompt) + } + }) + } +} +``` + +--- + +## Configuration Integration + +### Config Extension + +```go +// internal/appconfig/config.go + +type App struct { + // ... existing fields ... + + // MCP server settings + MCPPromptsDir string `json:"mcp_prompts_dir,omitempty" toml:"mcp_prompts_dir,omitempty"` + MCPIncludeBuiltin bool `json:"mcp_include_builtin" toml:"mcp_include_builtin"` + MCPCategories []string `json:"mcp_categories,omitempty" toml:"mcp_categories,omitempty"` + MCPMaxPromptsPerPage int `json:"mcp_max_prompts_per_page" toml:"mcp_max_prompts_per_page"` +} +``` + +### TOML Configuration + +```toml +# ~/.config/hexai/config.toml + +[mcp] +# Storage location for prompts +# Can be absolute path or relative to home directory +# Default: $XDG_DATA_HOME/hexai/prompts/ (usually ~/.local/share/hexai/prompts/) +# Examples: +# prompts_dir = "/home/user/git/my-prompts" # Absolute path +# prompts_dir = "~/Dropbox/hexai-prompts" # Home-relative +# prompts_dir = "" # Use default +prompts_dir = "" + +# Include built-in prompts (default: true) +include_builtin = true + +# Filter by categories (empty = all) +categories = [] + +# Max prompts per list response (pagination) +max_prompts_per_page = 50 +``` + +### Environment Variable Override + +```bash +# Override prompts directory via environment variable +export HEXAI_MCP_PROMPTS_DIR="/path/to/custom/prompts" +hexai-mcp-server +``` + +### Command-line Flag + +```bash +# Override prompts directory via command-line flag +hexai-mcp-server --prompts-dir /path/to/custom/prompts + +# Use with config file +hexai-mcp-server --config ~/.config/hexai/config.toml --prompts-dir ~/my-prompts +``` + +--- + +## Prompt Discovery and Search Strategy + +### How MCP Clients Find Prompts + +According to the MCP specification, the `prompts/list` method returns a list of prompts with metadata. Clients use this to discover available prompts. Here are multiple strategies for search/discovery: + +### 1. List-Based Discovery (MCP Standard) + +**How it works:** +- Client calls `prompts/list` to get all available prompts +- Each prompt includes: `name`, `title`, `description`, `arguments` +- Client presents list to user for selection +- User selects prompt, client calls `prompts/get` with name + +**Implementation:** +```go +// internal/mcp/handlers.go + +type PromptInfo struct { + Name string `json:"name"` // Unique ID + Title string `json:"title"` // Display name + Description string `json:"description"` // Human-readable + Arguments []PromptArgument `json:"arguments"` // Template vars +} + +func (s *Server) handlePromptsList(req Request) { + // Return all prompts with metadata + // Client does filtering client-side +} +``` + +**Pros:** +- Simple, follows MCP spec exactly +- No server-side search complexity +- Client can implement their own filtering +- Fast for small prompt collections (<1000) + +**Cons:** +- Doesn't scale to thousands of prompts +- No fuzzy matching +- Limited by client UI capabilities + +### 2. Tag-Based Filtering (Recommended) + +**How it works:** +- Each prompt has tags: `["development", "review", "testing"]` +- Client filters by tags before presenting to user +- Hierarchical categories possible: `"language/go"`, `"domain/web"` + +**Extended PromptInfo:** +```go +type PromptInfo struct { + Name string `json:"name"` + Title string `json:"title"` + Description string `json:"description"` + Arguments []PromptArgument `json:"arguments"` + Tags []string `json:"tags"` // NEW: categorization +} +``` + +**Search in PromptStore:** +```go +// internal/promptstore/store.go + +type PromptStore interface { + List(cursor string, limit int) ([]Prompt, string, error) + Get(name string) (*Prompt, error) + + // NEW: Search by tags (AND logic: all tags must match) + SearchByTags(tags []string) ([]Prompt, error) + + // NEW: Search by any tag (OR logic: any tag matches) + SearchByAnyTag(tags []string) ([]Prompt, error) +} +``` + +**Example tags:** +- `"language"`: go, python, rust, javascript +- `"domain"`: web, cli, api, database +- `"task"`: review, test, document, refactor, debug +- `"difficulty"`: beginner, intermediate, advanced + +**Pros:** +- Simple to implement +- Fast filtering (index tags) +- Human-organized categories +- Supports hierarchical organization + +**Cons:** +- Requires manual tagging +- No fuzzy matching +- Limited to predefined categories + +### 3. Full-Text Search (Enhanced) + +**How it works:** +- Search across: title, description, message content, argument descriptions +- Case-insensitive substring matching +- Rank results by relevance + +**Extended PromptStore:** +```go +// internal/promptstore/store.go + +type SearchOptions struct { + Query string // Search query + Fields []string // Which fields to search: "title", "description", "content" + Tags []string // Filter by tags + Limit int // Max results + MinScore float64 // Minimum relevance score (0-1) +} + +type SearchResult struct { + Prompt *Prompt + Score float64 // Relevance score (0-1) + Matches []Match // Where query was found +} + +type Match struct { + Field string // "title", "description", "content" + Context string // Surrounding text +} + +func (s *JSONLStore) Search(opts SearchOptions) ([]SearchResult, error) { + // 1. Load all prompts + // 2. For each prompt, score against query + // 3. Sort by score descending + // 4. Return top results +} +``` + +**Scoring algorithm (simple):** +```go +func scorePrompt(prompt *Prompt, query string) float64 { + query = strings.ToLower(query) + score := 0.0 + + // Title match (highest weight) + if strings.Contains(strings.ToLower(prompt.Title), query) { + score += 10.0 + } + + // Name match + if strings.Contains(strings.ToLower(prompt.Name), query) { + score += 8.0 + } + + // Description match + if strings.Contains(strings.ToLower(prompt.Description), query) { + score += 5.0 + } + + // Tag match + for _, tag := range prompt.Tags { + if strings.Contains(strings.ToLower(tag), query) { + score += 3.0 + } + } + + // Message content match (lowest weight) + for _, msg := range prompt.Messages { + if strings.Contains(strings.ToLower(msg.Content.Text), query) { + score += 1.0 + } + } + + return score +} +``` + +**Pros:** +- Natural language queries +- Searches all content +- Relevance ranking +- No manual tagging required + +**Cons:** +- Slower for large collections +- No fuzzy matching (yet) +- Simple scoring may not be accurate + +### 4. Fuzzy Search (Advanced, Future) + +**How it works:** +- Levenshtein distance for typo tolerance +- Phonetic matching (Soundex, Metaphone) +- Substring + fuzzy combined + +**Example library:** +```go +import "github.com/lithammer/fuzzysearch/fuzzy" + +func fuzzySearchPrompts(query string, prompts []Prompt) []SearchResult { + var results []SearchResult + for _, p := range prompts { + // Fuzzy match against title + if fuzzy.MatchFold(query, p.Title) { + distance := levenshtein.Distance(query, p.Title) + score := 1.0 - (float64(distance) / float64(len(p.Title))) + results = append(results, SearchResult{ + Prompt: &p, + Score: score, + }) + } + } + return results +} +``` + +**Pros:** +- Typo-tolerant +- Natural for users +- "code revie" matches "code review" + +**Cons:** +- More complex implementation +- Slower performance +- May match unwanted results + +### 5. MCP Best Practices (from Specification) + +According to MCP docs and existing servers: + +**Standard approach:** +1. `prompts/list` returns ALL prompts with full metadata +2. Client does filtering/search client-side +3. Keep prompt count reasonable (<100 per server) +4. Use clear, descriptive names and titles + +**For large collections (>100 prompts):** +1. Use pagination via `cursor` parameter +2. Consider namespace prefixes: `go/test`, `python/review` +3. Group related prompts into separate MCP servers +4. Example: `hexai-mcp-server-go` vs `hexai-mcp-server-python` + +**Metadata best practices:** +- **name**: slug-style, unique (`code_review`, `test_generator`) +- **title**: Human-readable ("Request Code Review", "Generate Unit Tests") +- **description**: 1-2 sentences explaining what it does +- **arguments**: Clear names and descriptions + +### Recommended Implementation (Phases) + +**Phase 1 (MVP):** List-based + Tags +- `prompts/list` returns all prompts with tags +- Client-side filtering by tags +- Simple, follows spec, works for <100 prompts + +**Phase 2:** Full-text search +- Add `Search(query string)` method to PromptStore +- Search title, description, tags +- Relevance scoring + +**Phase 3:** Advanced features +- Fuzzy matching +- Multi-language support +- Cached search index + +### Example Prompt Metadata Structure + +```json +{ + "name": "code_review_detailed", + "title": "Detailed Code Review", + "description": "Comprehensive code review covering style, performance, security, and best practices", + "tags": ["review", "quality", "security", "performance"], + "arguments": [ + { + "name": "code", + "description": "The code to review", + "required": true + }, + { + "name": "focus", + "description": "Specific aspect to focus on (security, performance, style, all)", + "required": false + } + ] +} +``` + +### Client-Side Search Example + +Most MCP clients will implement their own search UI: + +**Claude Code CLI example:** +``` +User types: /prompt code review +-> Claude Code searches locally cached prompts +-> Shows: "code_review", "code_review_detailed", "review_api" +-> User selects one +-> Claude Code calls prompts/get +``` + +**Cursor example:** +``` +User opens command palette, types "review" +-> Cursor filters MCP prompts by title/description +-> Shows matching prompts in dropdown +-> User selects, Cursor calls prompts/get +``` + +### Recommendation for hexai-mcp-server + +**Start with:** Tag-based categorization (Phase 1) +- Simple to implement +- Fast performance +- Good for initial prompt collections +- Extensible to full-text search later + +**Tag structure:** +```go +var builtinPrompts = []Prompt{ + { + Name: "code_review", + Title: "Request Code Review", + Tags: []string{"development", "review", "quality", "go"}, + // ... + }, + { + Name: "generate_tests", + Title: "Generate Unit Tests", + Tags: []string{"development", "testing", "go", "tdd"}, + // ... + }, +} +``` + +**Implementation:** +1. Include `tags` in PromptInfo returned by `prompts/list` +2. Client does tag filtering +3. Add `SearchByTags()` method for server-side filtering (optional) +4. Future: Add full-text search when needed + +--- + +## Built-in Prompts + +Initial set of useful prompts: + +1. **code_review** - Review code quality and suggest improvements +2. **explain_code** - Explain what code does in detail +3. **generate_tests** - Generate unit tests for a function/class +4. **document_function** - Generate documentation/docstrings +5. **simplify_code** - Simplify complex code while preserving behavior +6. **fix_bugs** - Analyze and suggest bug fixes +7. **refactor_extract** - Extract code into a separate function + +```go +// internal/promptstore/builtin.go + +var builtinPrompts = []Prompt{ + { + Name: "code_review", + Title: "Request Code Review", + Description: "Analyzes code quality, style, and suggests improvements", + Arguments: []PromptArgument{ + {Name: "code", Description: "The code to review", Required: true}, + }, + Messages: []PromptMessage{ + { + Role: "user", + Content: MessageContent{ + Type: "text", + Text: "Please review the following code for quality, style, and potential issues:\n\n{{code}}", + }, + }, + }, + Tags: []string{"development", "review", "quality"}, + }, + // ... more built-in prompts +} +``` + +--- + +## Agent Installation + +### Claude Code CLI + +Edit `~/.config/claude/mcp.json`: + +**Basic configuration (uses default prompts directory):** +```json +{ + "mcpServers": { + "hexai-prompts": { + "command": "/home/paul/go/bin/hexai-mcp-server", + "args": [], + "env": {} + } + } +} +``` + +**With custom prompts directory:** +```json +{ + "mcpServers": { + "hexai-prompts": { + "command": "/home/paul/go/bin/hexai-mcp-server", + "args": ["--prompts-dir", "/home/paul/Dropbox/hexai-prompts"], + "env": {} + } + } +} +``` + +**With environment variable:** +```json +{ + "mcpServers": { + "hexai-prompts": { + "command": "/home/paul/go/bin/hexai-mcp-server", + "args": [], + "env": { + "HEXAI_MCP_PROMPTS_DIR": "/home/paul/git/team-prompts" + } + } + } +} +``` + +**Alternative (if binary is in PATH):** +```json +{ + "mcpServers": { + "hexai-prompts": { + "command": "hexai-mcp-server", + "args": [], + "env": {} + } + } +} +``` + +### Cursor Agent + +Edit `~/.cursor/mcp.json`: + +**Basic configuration:** +```json +{ + "mcpServers": { + "hexai": { + "command": "/home/paul/go/bin/hexai-mcp-server", + "args": [], + "env": {} + } + } +} +``` + +**With custom config and prompts directory:** +```json +{ + "mcpServers": { + "hexai": { + "command": "/home/paul/go/bin/hexai-mcp-server", + "args": [ + "--config", "/home/paul/.config/hexai/config.toml", + "--prompts-dir", "/home/paul/git/shared-prompts" + ], + "env": {} + } + } +} +``` + +**Project-specific prompts via environment variable:** +```json +{ + "mcpServers": { + "hexai-project": { + "command": "/home/paul/go/bin/hexai-mcp-server", + "args": [], + "env": { + "HEXAI_MCP_PROMPTS_DIR": "${workspaceFolder}/.hexai/pro |
