summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-11 20:12:54 +0200
committerPaul Buetow <paul@buetow.org>2026-02-11 20:12:54 +0200
commit0a218306f8b3381610d219deca10a21406aa08cf (patch)
tree5ccbd55c7fc19234b0bd60668ee679d3cac40ea5
parentae38b11a09964e2c291a144c72814559d12d3b96 (diff)
Fix MCP transport to use JSONL instead of Content-Length framing
The MCP stdio protocol uses newline-delimited JSON (JSONL), not LSP-style Content-Length headers. This was discovered during integration testing with Claude Code CLI which could not connect to the server. Also updates docs/mcp-setup.md with correct Claude Code CLI configuration (use `claude mcp add` instead of editing JSON files) and adds integration test runbook for testing against real Claude Code CLI instances. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
-rw-r--r--docs/mcp-setup.md114
-rw-r--r--internal/mcp/server_test.go58
-rw-r--r--internal/mcp/transport.go62
-rw-r--r--prompts/mcp-server-integration-tests.md243
-rw-r--r--prompts/run-mcp-integration-test.md53
5 files changed, 406 insertions, 124 deletions
diff --git a/docs/mcp-setup.md b/docs/mcp-setup.md
index e151d77..4ea3a0b 100644
--- a/docs/mcp-setup.md
+++ b/docs/mcp-setup.md
@@ -32,60 +32,57 @@ The binary will be installed to `~/go/bin/hexai-mcp-server` (or wherever your `G
## Configuring Claude Code CLI
-Claude Code CLI discovers MCP servers via `~/.config/claude/mcp.json`. Create or edit this file:
+Claude Code CLI uses the `claude mcp add` command to register MCP servers. The configuration is stored in `~/.claude.json` (user scope) or `.mcp.json` (project scope).
-### Option 1: Full Path (Recommended)
+### Option 1: User Scope (Recommended)
-```json
-{
- "mcpServers": {
- "hexai-prompts": {
- "command": "/home/paul/go/bin/hexai-mcp-server",
- "args": [],
- "env": {}
- }
- }
-}
-```
+Register the MCP server for all projects:
-Replace `/home/paul` with your actual home directory path.
+```bash
+claude mcp add --transport stdio --scope user hexai-prompts -- \
+ ~/go/bin/hexai-mcp-server
+```
-### Option 2: Using $PATH
+### Option 2: Project Scope
-If `~/go/bin` is in your PATH:
+Register only for the current project (creates `.mcp.json` in project root):
-```json
-{
- "mcpServers": {
- "hexai-prompts": {
- "command": "hexai-mcp-server",
- "args": [],
- "env": {}
- }
- }
-}
+```bash
+claude mcp add --transport stdio --scope project hexai-prompts -- \
+ ~/go/bin/hexai-mcp-server
```
### Option 3: Custom Configuration
-```json
-{
- "mcpServers": {
- "hexai-prompts": {
- "command": "/home/paul/go/bin/hexai-mcp-server",
- "args": [
- "--config", "/home/paul/.config/hexai/config.toml",
- "--log", "/tmp/hexai-mcp-server.log"
- ],
- "env": {
- "HEXAI_MCP_PROMPTS_DIR": "/home/paul/Dropbox/hexai-prompts"
- }
- }
- }
-}
+Pass additional flags to the server:
+
+```bash
+claude mcp add --transport stdio --scope user hexai-prompts -- \
+ ~/go/bin/hexai-mcp-server \
+ --prompts-dir ~/Dropbox/hexai-prompts \
+ --log /tmp/hexai-mcp-server.log
+```
+
+### Verify Connection
+
+After adding, verify the server is connected:
+
+```bash
+claude mcp list
+# Expected: hexai-prompts: ✓ Connected
+```
+
+### Managing MCP Servers
+
+```bash
+# List all configured servers
+claude mcp list
+
+# Remove a server
+claude mcp remove hexai-prompts -s user
```
-**Configuration Options:**
+**Server Options:**
- `--config`: Path to hexai config file (optional)
- `--log`: Path to log file (default: `~/.local/state/hexai/hexai-mcp-server.log`)
- `--prompts-dir`: Directory for prompt storage (optional)
@@ -116,9 +113,9 @@ After configuring, restart Cursor to load the MCP server.
Any MCP-compatible client can use hexai-mcp-server. The general pattern is:
-1. Find the client's MCP configuration file (usually in `~/.config/<client>/mcp.json`)
+1. Find the client's MCP configuration method (CLI command or JSON config file)
2. Add an entry with the command path to `hexai-mcp-server`
-3. Restart the client
+3. Restart the client if required
## Prompts Directory
@@ -228,8 +225,9 @@ Common issues:
### Client Configuration Issues
**Claude Code CLI**:
-- Configuration file: `~/.config/claude/mcp.json`
-- Restart required after config changes
+- Configuration managed via `claude mcp add/remove/list` commands
+- User config stored in `~/.claude.json`, project config in `.mcp.json`
+- Restart Claude Code after config changes
**Cursor**:
- Configuration file: `~/.cursor/mcp.json`
@@ -240,23 +238,15 @@ Common issues:
### Multiple Prompt Collections
-You can run multiple instances of hexai-mcp-server with different prompt directories:
+You can register multiple instances of hexai-mcp-server with different prompt directories:
-```json
-{
- "mcpServers": {
- "hexai-general": {
- "command": "/home/paul/go/bin/hexai-mcp-server",
- "args": ["--prompts-dir", "/home/paul/prompts/general"],
- "env": {}
- },
- "hexai-go": {
- "command": "/home/paul/go/bin/hexai-mcp-server",
- "args": ["--prompts-dir", "/home/paul/prompts/go"],
- "env": {}
- }
- }
-}
+```bash
+claude mcp add --transport stdio --scope user hexai-general -- \
+ ~/go/bin/hexai-mcp-server --prompts-dir ~/prompts/general
+
+claude mcp add --transport stdio --scope user hexai-go -- \
+ ~/go/bin/hexai-mcp-server --prompts-dir ~/prompts/go
+```
```
### Shared Team Prompts
diff --git a/internal/mcp/server_test.go b/internal/mcp/server_test.go
index 4a14ffc..0c767f2 100644
--- a/internal/mcp/server_test.go
+++ b/internal/mcp/server_test.go
@@ -57,43 +57,34 @@ func createTestServer(t *testing.T, store promptstore.PromptStore) (*Server, *by
return NewServer(inBuf, outBuf, logger, store), inBuf, outBuf
}
+// sendRequest writes a JSON-RPC request as newline-delimited JSON (MCP stdio protocol).
func sendRequest(w io.Writer, req Request) error {
data, err := json.Marshal(req)
if err != nil {
return err
}
- header := fmt.Sprintf("Content-Length: %d\r\n\r\n", len(data))
- if _, err := io.WriteString(w, header); err != nil {
+ if _, err := w.Write(data); err != nil {
return err
}
- if _, err := w.Write(data); err != nil {
+ if _, err := io.WriteString(w, "\n"); err != nil {
return err
}
return nil
}
+// readResponse reads a newline-delimited JSON-RPC response (MCP stdio protocol).
func readResponse(r io.Reader) (*Response, error) {
- // Simple read for testing (assumes one message in buffer)
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
- // Find Content-Length
- lines := strings.Split(string(data), "\r\n")
- var contentLength int
- bodyStart := 0
- for i, line := range lines {
- if strings.HasPrefix(line, "Content-Length:") {
- fmt.Sscanf(line, "Content-Length: %d", &contentLength)
- }
- if line == "" {
- bodyStart = i + 1
- break
- }
+ // Parse newline-delimited JSON; take the last non-empty line as the response
+ lines := strings.Split(strings.TrimSpace(string(data)), "\n")
+ if len(lines) == 0 {
+ return nil, fmt.Errorf("no response data")
}
-
- body := strings.Join(lines[bodyStart:], "\r\n")
+ body := lines[len(lines)-1]
var resp Response
if err := json.Unmarshal([]byte(body), &resp); err != nil {
return nil, fmt.Errorf("unmarshal response: %w, body: %s", err, body)
@@ -459,11 +450,9 @@ func TestServer_ReadMessage(t *testing.T) {
logger := log.New(io.Discard, "", 0)
server := NewServer(inBuf, outBuf, logger, store)
- // Write a message with proper framing
- msg := []byte(`{"jsonrpc":"2.0","id":1,"method":"test"}`)
- header := fmt.Sprintf("Content-Length: %d\r\n\r\n", len(msg))
- inBuf.WriteString(header)
- inBuf.Write(msg)
+ // Write a newline-delimited JSON message (MCP stdio protocol)
+ msg := `{"jsonrpc":"2.0","id":1,"method":"test"}`
+ inBuf.WriteString(msg + "\n")
// Read it back
body, err := server.readMessage()
@@ -471,7 +460,28 @@ func TestServer_ReadMessage(t *testing.T) {
t.Fatalf("readMessage() error = %v", err)
}
- if string(body) != string(msg) {
+ if string(body) != msg {
+ t.Errorf("readMessage() = %s, want %s", body, msg)
+ }
+ })
+
+ t.Run("skips empty lines", func(t *testing.T) {
+ store := &mockPromptStore{prompts: make(map[string]*promptstore.Prompt)}
+ inBuf := &bytes.Buffer{}
+ outBuf := &bytes.Buffer{}
+ logger := log.New(io.Discard, "", 0)
+ server := NewServer(inBuf, outBuf, logger, store)
+
+ // Write empty lines followed by a valid message
+ msg := `{"jsonrpc":"2.0","id":1,"method":"test"}`
+ inBuf.WriteString("\n\n" + msg + "\n")
+
+ body, err := server.readMessage()
+ if err != nil {
+ t.Fatalf("readMessage() error = %v", err)
+ }
+
+ if string(body) != msg {
t.Errorf("readMessage() = %s, want %s", body, msg)
}
})
diff --git a/internal/mcp/transport.go b/internal/mcp/transport.go
index aba416a..4708e6d 100644
--- a/internal/mcp/transport.go
+++ b/internal/mcp/transport.go
@@ -1,54 +1,40 @@
-// Summary: MCP transport utilities for reading and writing JSON-RPC messages with Content-Length framing.
+// Summary: MCP transport utilities for reading and writing JSON-RPC messages
+// using newline-delimited JSON (JSONL) as required by the MCP stdio protocol.
package mcp
import (
"encoding/json"
"fmt"
"io"
- "net/textproto"
- "strconv"
"strings"
)
-// readMessage reads a Content-Length framed JSON-RPC message from the input stream.
-// Returns the raw JSON bytes. Follows LSP/JSON-RPC framing convention.
+// readMessage reads a newline-delimited JSON-RPC message from the input stream.
+// Returns the raw JSON bytes. Follows the MCP stdio transport specification
+// which uses newline-delimited JSON (JSONL), not LSP-style Content-Length framing.
+// Uses the server's bufio.Reader (s.in) to avoid losing buffered data between calls.
func (s *Server) readMessage() ([]byte, error) {
- tp := textproto.NewReader(s.in)
- var contentLength int
for {
- line, err := tp.ReadLine()
- if err != nil {
- return nil, err
- }
- if line == "" { // end of headers
- break
- }
- parts := strings.SplitN(line, ":", 2)
- if len(parts) != 2 {
- continue
+ line, err := s.in.ReadString('\n')
+ if err != nil && len(line) == 0 {
+ if err == io.EOF {
+ return nil, io.EOF
+ }
+ return nil, fmt.Errorf("read error: %w", err)
}
- key := strings.TrimSpace(strings.ToLower(parts[0]))
- val := strings.TrimSpace(parts[1])
- switch key {
- case "content-length":
- n, err := strconv.Atoi(val)
- if err != nil {
- return nil, fmt.Errorf("invalid Content-Length: %v", err)
+ line = strings.TrimSpace(line)
+ if line == "" {
+ // If we hit EOF on an empty line, propagate it
+ if err == io.EOF {
+ return nil, io.EOF
}
- contentLength = n
+ continue // skip empty lines
}
+ return []byte(line), nil
}
- if contentLength <= 0 {
- return nil, fmt.Errorf("missing or invalid Content-Length")
- }
- buf := make([]byte, contentLength)
- if _, err := io.ReadFull(s.in, buf); err != nil {
- return nil, err
- }
- return buf, nil
}
-// writeMessage writes a JSON-RPC response with Content-Length framing.
+// writeMessage writes a JSON-RPC response as newline-delimited JSON.
// Thread-safe via mutex lock.
func (s *Server) writeMessage(v any) error {
s.outMu.Lock()
@@ -58,12 +44,12 @@ func (s *Server) writeMessage(v any) error {
if err != nil {
return fmt.Errorf("marshal error: %w", err)
}
- header := fmt.Sprintf("Content-Length: %d\r\n\r\n", len(data))
- if _, err := io.WriteString(s.out, header); err != nil {
- return fmt.Errorf("write header error: %w", err)
- }
+ // Write JSON followed by newline (JSONL format)
if _, err := s.out.Write(data); err != nil {
return fmt.Errorf("write body error: %w", err)
}
+ if _, err := io.WriteString(s.out, "\n"); err != nil {
+ return fmt.Errorf("write newline error: %w", err)
+ }
return nil
}
diff --git a/prompts/mcp-server-integration-tests.md b/prompts/mcp-server-integration-tests.md
new file mode 100644
index 0000000..cb24a8f
--- /dev/null
+++ b/prompts/mcp-server-integration-tests.md
@@ -0,0 +1,243 @@
+# hexai-mcp-server Integration Test Runbook
+
+Real-life integration tests against Claude Code CLI using the hexai MCP server.
+These tests verify the full MCP protocol integration: server discovery, prompt
+listing/retrieval via slash commands, and file persistence across sessions.
+
+**Protocol**: The MCP stdio transport uses newline-delimited JSON (JSONL), not
+LSP-style Content-Length framing. Each message is a single JSON object followed
+by a newline character.
+
+**Test Status**:
+- ✓ **Claude Code CLI**: Tested and working (2026-02-11)
+- ⏳ **Cursor**: Needs testing (future)
+
+## Prerequisites
+
+- Must be running inside a tmux session
+- Claude Code CLI must be installed and authenticated
+- `hexai-mcp-server` binary must be built: `go build -o ~/go/bin/hexai-mcp-server ./cmd/hexai-mcp-server/`
+- `jq` command-line JSON processor installed
+- All unit tests must pass first: `go test ./internal/mcp/` and `go test ./internal/hexaimcp/`
+
+## Setup: Create Test Environment
+
+Before running tests, set up an isolated test environment:
+
+```sh
+# 1. Create temporary prompts directory for testing
+TEST_PROMPTS_DIR="/tmp/hexai-mcp-test-$(date +%s)"
+mkdir -p "$TEST_PROMPTS_DIR"
+echo "Test prompts directory: $TEST_PROMPTS_DIR"
+
+# 2. Create minimal test prompts in JSONL format
+cat > "$TEST_PROMPTS_DIR/default.jsonl" << 'EOF'
+{"name":"test_review","title":"Test Code Review","description":"A test prompt for code review","arguments":[{"name":"code","description":"Code to review","required":true}],"messages":[{"role":"user","content":{"type":"text","text":"Review this code:\n\n{{code}}\n\nProvide:\n1. Issues found\n2. Suggestions for improvement"}}],"tags":["testing","code-quality"],"created":"2026-02-10T00:00:00Z","updated":"2026-02-10T00:00:00Z"}
+{"name":"test_explain","title":"Test Explain Code","description":"A test prompt to explain code","arguments":[{"name":"code","description":"Code to explain","required":true}],"messages":[{"role":"user","content":{"type":"text","text":"Explain what this code does:\n\n{{code}}"}}],"tags":["testing","documentation"],"created":"2026-02-10T00:00:00Z","updated":"2026-02-10T00:00:00Z"}
+EOF
+
+# 3. Create empty user.jsonl (for testing creation)
+touch "$TEST_PROMPTS_DIR/user.jsonl"
+
+# 4. Register test MCP server with Claude Code CLI (user scope)
+claude mcp add --transport stdio --scope user hexai-test -- \
+ "$HOME/go/bin/hexai-mcp-server" \
+ --prompts-dir "$TEST_PROMPTS_DIR" \
+ --log /tmp/hexai-mcp-test.log
+echo "✓ Registered test MCP server"
+
+# 5. Verify server is connected
+claude mcp list | grep hexai-test
+# Expected: hexai-test: ✓ Connected
+
+# 6. Start Claude Code CLI in a new tmux pane
+CLAUDE_PANE=$(tmux split-window -v -d -P -F '#{pane_id}' 'claude')
+sleep 3
+echo "Claude running in pane: $CLAUDE_PANE"
+
+# 7. Verify MCP server started (check logs)
+if grep -q "hexai-mcp-server starting" /tmp/hexai-mcp-test.log 2>/dev/null; then
+ echo "✓ MCP server started successfully"
+else
+ echo "✗ MCP server failed to start (check /tmp/hexai-mcp-test.log)"
+fi
+```
+
+Expected state after setup:
+- Test prompts directory created with 2 default prompts
+- MCP server registered via `claude mcp add`
+- Claude Code CLI running in tmux pane
+- MCP server initialized and connected
+
+## Test 1: Server Connection and Initialization
+
+**Goal**: Verify that Claude Code successfully connects to the MCP server
+and completes the initialization handshake.
+
+```sh
+# 1. Check MCP server log for initialization sequence
+grep "initialize" /tmp/hexai-mcp-test.log
+# Expected: Log entries showing initialization from client
+
+# 2. Check for any connection errors
+grep -i "error\|fail" /tmp/hexai-mcp-test.log || echo "✓ No errors in log"
+
+# 3. Verify Claude is responsive in the pane
+tmux send-keys -t "$CLAUDE_PANE" "test connection"
+sleep 1
+tmux capture-pane -p -t "$CLAUDE_PANE" | tail -5
+```
+
+## Test 2: Prompt Discovery via Slash Commands
+
+**Goal**: Verify that prompts from the MCP server appear as slash commands
+in Claude Code CLI. MCP prompts appear as `/hexai-test:<prompt_name>`.
+
+```sh
+# 1. Type the slash command prefix to trigger autocomplete
+tmux send-keys -t "$CLAUDE_PANE" "/hexai-test:"
+sleep 2
+
+# 2. Capture the autocomplete suggestions
+tmux capture-pane -p -t "$CLAUDE_PANE" -S -20
+# Expected: Should show test_review and test_explain as available commands
+
+# 3. Press Escape to clear
+tmux send-keys -t "$CLAUDE_PANE" Escape
+sleep 0.5
+
+# 4. Check MCP server log for prompts/list request
+grep "prompts/list" /tmp/hexai-mcp-test.log
+```
+
+## Test 3: Prompt Retrieval via Slash Command
+
+**Goal**: Verify that a specific prompt can be retrieved and used via
+the slash command mechanism with argument substitution.
+
+```sh
+# 1. Use the test_review prompt via slash command
+tmux send-keys -t "$CLAUDE_PANE" "/hexai-test:test_review"
+sleep 1
+# Select the prompt from autocomplete if needed
+tmux send-keys -t "$CLAUDE_PANE" Enter
+sleep 1
+
+# 2. Fill in the code argument when prompted
+tmux send-keys -t "$CLAUDE_PANE" "func Add(a, b int) int { return a + b }"
+tmux send-keys -t "$CLAUDE_PANE" Enter
+sleep 4
+
+# 3. Check MCP server log for prompts/get request
+grep "prompts/get" /tmp/hexai-mcp-test.log | tail -1
+
+# 4. Capture Claude's response
+tmux capture-pane -p -t "$CLAUDE_PANE" -S -30 | tail -15
+
+# 5. Clear for next test
+tmux send-keys -t "$CLAUDE_PANE" Escape
+sleep 0.5
+```
+
+## Test 4: Persistence Across Sessions
+
+**Goal**: Verify that prompts persist when starting a new Claude Code CLI session.
+
+```sh
+# 1. Kill the current Claude pane
+tmux kill-pane -t "$CLAUDE_PANE"
+sleep 1
+
+# 2. Start a fresh Claude session
+CLAUDE_PANE=$(tmux split-window -v -d -P -F '#{pane_id}' 'claude')
+sleep 3
+
+# 3. Verify the MCP server reconnects (check new log entries)
+grep "initialize" /tmp/hexai-mcp-test.log | wc -l
+# Expected: Should show 2 or more initialization entries
+
+# 4. Try using the prompt in the new session
+tmux send-keys -t "$CLAUDE_PANE" "/hexai-test:test_review"
+sleep 2
+tmux capture-pane -p -t "$CLAUDE_PANE" -S -10
+# Expected: Prompt should appear in autocomplete
+
+# 5. Clear
+tmux send-keys -t "$CLAUDE_PANE" Escape
+sleep 0.5
+```
+
+## Test 5: Error Handling
+
+**Goal**: Verify proper error messages for invalid operations.
+
+```sh
+# Test 5a: Get non-existent prompt
+tmux send-keys -t "$CLAUDE_PANE" "Get the prompt called 'nonexistent_prompt' from the hexai-test MCP server"
+tmux send-keys -t "$CLAUDE_PANE" Enter
+sleep 3
+
+# Check for error in logs
+grep -i "not found" /tmp/hexai-mcp-test.log | tail -1
+
+tmux send-keys -t "$CLAUDE_PANE" Escape
+sleep 0.5
+```
+
+## Cleanup
+
+After all tests, remove the test server and clean up:
+
+```sh
+# 1. Kill Claude pane
+if [ -n "$CLAUDE_PANE" ]; then
+ tmux kill-pane -t "$CLAUDE_PANE"
+ echo "✓ Closed Claude pane: $CLAUDE_PANE"
+fi
+
+# 2. Remove test MCP server registration
+claude mcp remove hexai-test -s user
+echo "✓ Removed test MCP server"
+
+# 3. Keep test directory and logs for inspection
+echo "Test artifacts preserved:"
+echo " Prompts: $TEST_PROMPTS_DIR"
+echo " Log: /tmp/hexai-mcp-test.log"
+echo ""
+echo "To clean up manually:"
+echo " rm -rf $TEST_PROMPTS_DIR"
+echo " rm /tmp/hexai-mcp-test.log"
+
+# 4. Check for hung MCP server processes
+HUNG_PROCS=$(pgrep -f hexai-mcp-server | wc -l)
+if [ "$HUNG_PROCS" -gt 0 ]; then
+ echo "⚠ Warning: $HUNG_PROCS hexai-mcp-server processes still running"
+ echo "Run: pkill -f hexai-mcp-server"
+else
+ echo "✓ No hung MCP server processes"
+fi
+```
+
+## Troubleshooting
+
+**Claude doesn't see MCP server**:
+- Verify registration: `claude mcp list` should show `hexai-test: ✓ Connected`
+- If not connected, re-register: `claude mcp add --transport stdio --scope user hexai-test -- ...`
+- Restart Claude Code CLI (kill pane and start new one)
+- Check `/tmp/hexai-mcp-test.log` for startup errors
+
+**MCP server not starting**:
+- Verify binary is executable: `ls -la ~/go/bin/hexai-mcp-server`
+- Check prompts directory exists: `ls -la $TEST_PROMPTS_DIR`
+- Try running manually: `echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | hexai-mcp-server --prompts-dir $TEST_PROMPTS_DIR --log /tmp/manual-test.log`
+
+**Prompts not appearing as slash commands**:
+- MCP prompts appear as `/hexai-test:<name>` in Claude Code CLI
+- Type `/hexai-test:` and wait for autocomplete
+- Check prompts directory has valid JSONL files
+- Verify JSONL: `jq -s '.' $TEST_PROMPTS_DIR/default.jsonl`
+
+**Protocol errors**:
+- The MCP stdio transport uses JSONL (newline-delimited JSON), not Content-Length framing
+- Check JSON-RPC format in logs
+- Test with: `echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | hexai-mcp-server`
diff --git a/prompts/run-mcp-integration-test.md b/prompts/run-mcp-integration-test.md
new file mode 100644
index 0000000..23aeb00
--- /dev/null
+++ b/prompts/run-mcp-integration-test.md
@@ -0,0 +1,53 @@
+# Prompt: Run MCP Server Integration Tests
+
+**Context**: You are running integration tests for the hexai-mcp-server against a real Claude Code CLI instance in tmux. These tests verify the full Model Context Protocol integration including prompt discovery via slash commands and persistence across sessions.
+
+**Your Role**: Execute each test systematically, report results clearly, and identify any failures.
+
+## Instructions
+
+Read the test runbook at `prompts/mcp-server-integration-tests.md` and execute each test in order. For each test:
+
+1. **Announce** what you're testing
+2. **Execute** the test commands
+3. **Verify** the expected outcomes
+4. **Report** results clearly (PASS or FAIL)
+5. **Investigate** any failures before moving to next test
+
+## Test Execution Flow
+
+### Phase 1: Setup
+- Create test environment (temp directory, test prompts)
+- Register MCP server via `claude mcp add --transport stdio --scope user`
+- Start Claude Code CLI in tmux pane
+- Verify MCP server connection via `claude mcp list`
+
+### Phase 2: Core Tests
+Execute tests 1-5 in sequence:
+1. Server Connection and Initialization
+2. Prompt Discovery via Slash Commands (`/hexai-test:<name>`)
+3. Prompt Retrieval via Slash Command
+4. Persistence Across Sessions (kill and restart Claude)
+5. Error Handling
+
+### Phase 3: Cleanup
+- Kill Claude pane
+- Remove test MCP server: `claude mcp remove hexai-test -s user`
+- Report final summary
+
+## Key Details
+
+- MCP prompts appear as slash commands: `/hexai-test:<prompt_name>`
+- The MCP stdio transport uses JSONL (newline-delimited JSON), not Content-Length
+- Register servers with `claude mcp add`, not by editing JSON config files
+- Always clean up: kill panes and remove test server registration
+
+## After Completion
+
+Report:
+1. **Overall result**: X/5 tests passed
+2. **Failures**: List any failed tests with details
+3. **Artifacts location**: Where logs and test data are stored
+4. **Recommendations**: Any bugs found or improvements needed
+
+Start with Phase 1 (Setup) and proceed systematically through all phases.