Skip to content

feat(mcp): ghost mcp init for Claude Code integration#110

Merged
wcatz merged 1 commit intomainfrom
feat/mcp-init
Mar 23, 2026
Merged

feat(mcp): ghost mcp init for Claude Code integration#110
wcatz merged 1 commit intomainfrom
feat/mcp-init

Conversation

@wcatz
Copy link
Copy Markdown
Owner

@wcatz wcatz commented Mar 23, 2026

Summary

  • Adds ghost mcp init — a single command that fully configures Claude Code to use Ghost as its memory system
  • Adds ghost hook session-start — a lightweight SessionStart hook that reminds Claude to load Ghost context
  • Exports EncodeProjectPath from claudeimport package for reuse

What ghost mcp init does (6 steps):

  1. Verifies ghost and claude binaries are on PATH
  2. Registers Ghost MCP server via claude mcp add-json
  3. Adds all 13 mcp__ghost__* tool permissions to ~/.claude/settings.json
  4. Configures a SessionStart hook that reminds Claude to call ghost_project_context
  5. Imports existing Claude Code memory files into Ghost (deduplicated via upsert)
  6. Writes MEMORY.md redirect files for all known projects

Idempotent — safe to re-run after updates.

Test plan

  • go vet ./... clean
  • go test ./... — all pass (15 new tests in mcpinit + existing tests unchanged)
  • go build ./cmd/ghost/ compiles
  • Manual: ghost mcp init — verify output, check settings.json, check MEMORY.md files
  • Manual: new Claude Code session — verify SessionStart hook fires

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added ghost mcp init command for streamlined Claude Code integration setup in a single operation
    • Automatic MCP server registration and tool permission configuration
    • Support for importing existing Claude Code memory files into Ghost with deduplication
    • Session start hook integration for seamless Claude Code workflow
  • Documentation

    • Updated README with comprehensive MCP setup guide and requirements

Adds a one-command setup for Claude Code:
- Registers Ghost MCP server via claude CLI
- Adds all 13 tool permissions to settings.json
- Configures SessionStart hook for context reminders
- Imports existing Claude Code memory files into Ghost
- Writes project memory redirects pointing to Ghost

New subcommands: ghost mcp init, ghost hook session-start
New package: internal/mcpinit (init, settings, hook + tests)
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 23, 2026

📝 Walkthrough

Walkthrough

This PR introduces a new ghost mcp init command that automates the setup of Ghost with Claude Code via MCP. The workflow verifies prerequisites (ghost and claude CLI tools), registers the MCP server, configures tool permissions in Claude settings, adds a SessionStart hook, imports existing memories, and writes memory redirects to the Claude projects directory.

Changes

Cohort / File(s) Summary
Documentation
README.md
Added setup guide describing the ghost mcp init one-command workflow, including MCP server registration, tool permission allow-listing for 13 tools, and memory import/redirect creation. Removed statement that no CLAUDE.md configuration is required.
Command Handler
cmd/ghost/main.go
Added subcommand routing for ghost mcp init and ghost hook session-start, with implementations calling new runMCPInit() and runHook() functions that delegate to the mcpinit package.
Exported Helper
internal/claudeimport/import.go, internal/claudeimport/import_test.go
Promoted unexported encodeProjectPath() to exported EncodeProjectPath() function and updated all call sites and test assertions accordingly.
MCP Init Package
internal/mcpinit/hook.go, internal/mcpinit/init.go, internal/mcpinit/init_test.go, internal/mcpinit/settings.go, internal/mcpinit/settings_test.go
Added new mcpinit package implementing a 6-step initialization workflow: check prerequisites, register MCP server, configure permissions, add SessionStart hook, import memories from Ghost database, and write MEMORY.md redirects. Includes settings file abstraction for safe JSON manipulation and comprehensive unit tests.

Sequence Diagram

sequenceDiagram
    actor User
    participant Ghost as Ghost CLI
    participant MCPInit as mcpinit.Run()
    participant Claude as Claude CLI
    participant FS as File System
    participant GhostDB as Ghost Database

    User->>Ghost: ghost mcp init
    Ghost->>MCPInit: Execute initialization workflow
    
    MCPInit->>Claude: Check prerequisites (ghost, claude binaries)
    Claude-->>MCPInit: ✓ Both found on PATH
    
    MCPInit->>Claude: Query existing MCP server (claude mcp get ghost)
    Claude-->>MCPInit: Not registered or config mismatch
    MCPInit->>Claude: Register/update MCP server (claude mcp add-json ...)
    Claude-->>MCPInit: ✓ Registered
    
    MCPInit->>FS: Load ~/.claude/settings.json
    FS-->>MCPInit: Settings loaded
    MCPInit->>MCPInit: Add missing ghost permissions (13 tools)
    MCPInit->>FS: Save updated settings.json (with .bak backup)
    FS-->>MCPInit: ✓ Saved
    
    MCPInit->>FS: Check SessionStart hook in settings
    FS-->>MCPInit: Hook not present
    MCPInit->>FS: Add SessionStart hook to settings.json
    FS-->>MCPInit: ✓ Hook added
    
    MCPInit->>GhostDB: Open ghost.db
    GhostDB-->>MCPInit: Database connection
    MCPInit->>GhostDB: List projects
    GhostDB-->>MCPInit: Project list returned
    
    loop For each project
        MCPInit->>MCPInit: Import memories via claudeimport
        MCPInit->>FS: Create ~/.claude/projects/<encoded>/memory/ directory
    end
    MCPInit->>FS: Write MEMORY.md redirects
    FS-->>MCPInit: ✓ Redirects written
    
    MCPInit-->>Ghost: ✓ Setup complete
    Ghost-->>User: Exit (success)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 A rabbit hops through MCP gates,
Six swift steps automate!
Ghost meets Claude in harmony,
Settings, permissions, memory—
One command weaves the magic thread! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(mcp): ghost mcp init for Claude Code integration' accurately and specifically summarizes the main change: adding a new CLI command for MCP initialization.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mcp-init

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
cmd/ghost/main.go (1)

457-466: Clarify behavior for unknown hook subcommands.

The runHook function silently exits with code 0 when no argument is provided or when an unknown hook type is passed (e.g., ghost hook unknown). This appears intentional to avoid breaking Claude Code if new hook types are added, but consider adding a debug log or comment to clarify this design choice.

💡 Optional: Add a comment explaining the silent fallthrough
 // runHook dispatches Claude Code hook events.
+// Unknown hook types are silently ignored to maintain forward compatibility
+// with future Claude Code hook events.
 func runHook() {
 	if len(os.Args) < 3 {
 		os.Exit(0)
 	}
 	switch os.Args[2] {
 	case "session-start":
 		mcpinit.HandleSessionStartHook(os.Stdin, os.Stdout)
+	// Other hook types (e.g., PreToolUse, PostToolUse) are not handled.
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/ghost/main.go` around lines 457 - 466, The runHook function currently
returns silently for missing or unrecognized hook subcommands, which is
ambiguous; update runHook to explicitly document or log this behavior: inside
runHook (function runHook) add a brief comment explaining the intentional silent
fallthrough for unknown hooks and, optionally, emit a debug-level log (e.g.,
using the existing logger) when len(os.Args) < 3 or when the switch hits no case
(unknown subcommand) so callers can see why execution exits without error—keep
the current exit(0) behavior but make it explicit via comment and/or a debug log
surrounding the mcpinit.HandleSessionStartHook call.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/mcpinit/hook.go`:
- Around line 14-19: The function HandleSessionStartHook currently ignores the
error returns from fmt.Fprintln (causing errcheck CI failures); update
HandleSessionStartHook to explicitly handle those returns — e.g., capture the
error values from each fmt.Fprintln call and handle or log them (or assign to _
if you intentionally ignore them) and do the same for the io.Copy call if
desired; ensure the error variables are referenced so the linter is satisfied
while preserving the current behavior and keep references to
HandleSessionStartHook and fmt.Fprintln in your change.

In `@internal/mcpinit/init.go`:
- Around line 201-205: Replace the plain defer db.Close() with a deferred
closure that calls db.Close(), checks its returned error, and logs any failure
to the writer passed into importMemories (w) so close errors are not ignored;
locate the db variable opened by memory.OpenDB in init.go and update the defer
to something like a deferred func that invokes db.Close(), captures the error,
and writes a descriptive message including the error to w (use fmt.Fprintf or
fmt.Fprintln) referencing db and importMemories in the message for context.
- Around line 290-296: The MEMORY.md content uses the sanitized display name
(safeName from sanitizeName(p.Name)) for the ghost_project_context project_id
placeholder, which is wrong; update the fmt.Sprintf call that constructs content
(variable content) to keep safeName for the header but pass p.ID (the actual
project identifier) into the "%s" used in the `ghost_project_context`
instruction so the generated command uses the real project ID instead of the
sanitized name.

---

Nitpick comments:
In `@cmd/ghost/main.go`:
- Around line 457-466: The runHook function currently returns silently for
missing or unrecognized hook subcommands, which is ambiguous; update runHook to
explicitly document or log this behavior: inside runHook (function runHook) add
a brief comment explaining the intentional silent fallthrough for unknown hooks
and, optionally, emit a debug-level log (e.g., using the existing logger) when
len(os.Args) < 3 or when the switch hits no case (unknown subcommand) so callers
can see why execution exits without error—keep the current exit(0) behavior but
make it explicit via comment and/or a debug log surrounding the
mcpinit.HandleSessionStartHook call.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 76ce50ce-8a87-4577-96cd-57d9e1351521

📥 Commits

Reviewing files that changed from the base of the PR and between 07dd7f2 and 64e7177.

📒 Files selected for processing (9)
  • README.md
  • cmd/ghost/main.go
  • internal/claudeimport/import.go
  • internal/claudeimport/import_test.go
  • internal/mcpinit/hook.go
  • internal/mcpinit/init.go
  • internal/mcpinit/init_test.go
  • internal/mcpinit/settings.go
  • internal/mcpinit/settings_test.go

Comment thread internal/mcpinit/hook.go
Comment on lines +14 to +19
func HandleSessionStartHook(stdin io.Reader, stdout io.Writer) {
_, _ = io.Copy(io.Discard, stdin) // drain stdin
fmt.Fprintln(stdout, "Ghost memory is active. Before starting work:")
fmt.Fprintln(stdout, "1. Call ghost_project_context with the project name")
fmt.Fprintln(stdout, "2. Save discoveries with ghost_memory_save during work")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle fmt.Fprintln return values to fix CI failure.

The pipeline fails due to unchecked error return values from fmt.Fprintln. While stdout writes rarely fail, the linter requires explicit handling.

🔧 Proposed fix to satisfy errcheck
 func HandleSessionStartHook(stdin io.Reader, stdout io.Writer) {
 	_, _ = io.Copy(io.Discard, stdin) // drain stdin
-	fmt.Fprintln(stdout, "Ghost memory is active. Before starting work:")
-	fmt.Fprintln(stdout, "1. Call ghost_project_context with the project name")
-	fmt.Fprintln(stdout, "2. Save discoveries with ghost_memory_save during work")
+	_, _ = fmt.Fprintln(stdout, "Ghost memory is active. Before starting work:")
+	_, _ = fmt.Fprintln(stdout, "1. Call ghost_project_context with the project name")
+	_, _ = fmt.Fprintln(stdout, "2. Save discoveries with ghost_memory_save during work")
 }
🧰 Tools
🪛 GitHub Actions: ci

[error] 16-16: golangci-lint: Error return value of fmt.Fprintln is not checked (errcheck)

🪛 GitHub Check: build-and-test

[failure] 18-18:
Error return value of fmt.Fprintln is not checked (errcheck)


[failure] 17-17:
Error return value of fmt.Fprintln is not checked (errcheck)


[failure] 16-16:
Error return value of fmt.Fprintln is not checked (errcheck)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/mcpinit/hook.go` around lines 14 - 19, The function
HandleSessionStartHook currently ignores the error returns from fmt.Fprintln
(causing errcheck CI failures); update HandleSessionStartHook to explicitly
handle those returns — e.g., capture the error values from each fmt.Fprintln
call and handle or log them (or assign to _ if you intentionally ignore them)
and do the same for the io.Copy call if desired; ensure the error variables are
referenced so the linter is satisfied while preserving the current behavior and
keep references to HandleSessionStartHook and fmt.Fprintln in your change.

Comment thread internal/mcpinit/init.go
Comment on lines +201 to +205
db, err := memory.OpenDB(dbPath)
if err != nil {
return nil, fmt.Errorf("open db: %w", err)
}
defer db.Close()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle db.Close() error in defer.

The static analysis correctly flagged that db.Close() error is unchecked. While this is a read-only operation and errors are rare, it's good practice to at least log potential close failures.

Proposed fix
 	db, err := memory.OpenDB(dbPath)
 	if err != nil {
 		return nil, fmt.Errorf("open db: %w", err)
 	}
-	defer db.Close()
+	defer func() {
+		if cerr := db.Close(); cerr != nil {
+			fmt.Fprintf(w, "  ! warning: db close: %v\n", cerr)
+		}
+	}()

Note: This requires changing the function signature to accept w io.Writer or capturing it in the closure scope. Since w is already passed to importMemories, you can use it directly in the defer closure.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
db, err := memory.OpenDB(dbPath)
if err != nil {
return nil, fmt.Errorf("open db: %w", err)
}
defer db.Close()
db, err := memory.OpenDB(dbPath)
if err != nil {
return nil, fmt.Errorf("open db: %w", err)
}
defer func() {
if cerr := db.Close(); cerr != nil {
fmt.Fprintf(w, " ! warning: db close: %v\n", cerr)
}
}()
🧰 Tools
🪛 GitHub Check: build-and-test

[failure] 205-205:
Error return value of db.Close is not checked (errcheck)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/mcpinit/init.go` around lines 201 - 205, Replace the plain defer
db.Close() with a deferred closure that calls db.Close(), checks its returned
error, and logs any failure to the writer passed into importMemories (w) so
close errors are not ignored; locate the db variable opened by memory.OpenDB in
init.go and update the defer to something like a deferred func that invokes
db.Close(), captures the error, and writes a descriptive message including the
error to w (use fmt.Fprintf or fmt.Fprintln) referencing db and importMemories
in the message for context.

Comment thread internal/mcpinit/init.go
Comment on lines +290 to +296
safeName := sanitizeName(p.Name)
content := fmt.Sprintf(`# %s Project Memory

All project knowledge is stored in Ghost. At session start, run:
1. `+"`ghost_list_projects`"+` to discover projects
2. `+"`ghost_project_context`"+` with project_id "%s" to load accumulated knowledge
`, safeName, safeName)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: Using sanitized name instead of project ID.

The project_id parameter in the MEMORY.md instructions uses safeName (the sanitized display name) instead of p.ID (the actual project identifier). This will cause ghost_project_context calls to fail since the name won't match any project ID in the database.

Proposed fix
 		safeName := sanitizeName(p.Name)
 		content := fmt.Sprintf(`# %s Project Memory

 All project knowledge is stored in Ghost. At session start, run:
 1. `+"`ghost_list_projects`"+` to discover projects
-2. `+"`ghost_project_context`"+` with project_id "%s" to load accumulated knowledge
-`, safeName, safeName)
+2. `+"`ghost_project_context`"+` with project_id "%s" to load accumulated knowledge
+`, safeName, p.ID)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
safeName := sanitizeName(p.Name)
content := fmt.Sprintf(`# %s Project Memory
All project knowledge is stored in Ghost. At session start, run:
1. `+"`ghost_list_projects`"+` to discover projects
2. `+"`ghost_project_context`"+` with project_id "%s" to load accumulated knowledge
`, safeName, safeName)
safeName := sanitizeName(p.Name)
content := fmt.Sprintf(`# %s Project Memory
All project knowledge is stored in Ghost. At session start, run:
1. `+"`ghost_list_projects`"+` to discover projects
2. `+"`ghost_project_context`"+` with project_id "%s" to load accumulated knowledge
`, safeName, p.ID)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/mcpinit/init.go` around lines 290 - 296, The MEMORY.md content uses
the sanitized display name (safeName from sanitizeName(p.Name)) for the
ghost_project_context project_id placeholder, which is wrong; update the
fmt.Sprintf call that constructs content (variable content) to keep safeName for
the header but pass p.ID (the actual project identifier) into the "%s" used in
the `ghost_project_context` instruction so the generated command uses the real
project ID instead of the sanitized name.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant