mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-10-17 05:59:36 +00:00
general improvements (#17)
* refactor(hooks): simplify hook execution with shared context - [x] Extract BaseInput struct to eliminate duplicate fields across hooks - [x] Create RunHook handler pattern for session-start and user-prompt - [x] Create RunStatuslineHook for fast statusline rendering without worker startup - [x] Add HookContext struct to pass port, project, CWD, SessionID to handlers - [x] Add db/interface.go with ObservationReader/Writer interfaces - [x] Add comprehensive conflict management tests in sqlite/conflict_test.go - [x] Add vector client tests for Count, ModelVersion, NeedsRebuild, GetStaleVectors - [x] Add FilterByThreshold helper tests for query result filtering - [x] Make handlers_test more robust for network-dependent update checks - [x] Update package versions in UI * Move to GORM + general cleanup * feat(mcp): add observation relations discovery and scoring integration - [x] Add find_related_observations MCP tool for discovering related observations by confidence - [x] Integrate scoring calculator and recalculator into MCP server initialization - [x] Add pattern, relation, and session stores to MCP server dependencies - [x] Register MCP server in Claude Code settings during plugin installation - [x] Update install scripts (bash, PowerShell) to configure MCP server settings - [x] Switch plugin manifest files to template-based versioning (plugin.json.tpl, marketplace.json.tpl) - [x] Update all MCP server tests to pass new dependency parameters
This commit is contained in:
+120
-5
@@ -9,7 +9,11 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/lukaszraczylo/claude-mnemonic/internal/db/gorm"
|
||||
"github.com/lukaszraczylo/claude-mnemonic/internal/scoring"
|
||||
"github.com/lukaszraczylo/claude-mnemonic/internal/search"
|
||||
"github.com/lukaszraczylo/claude-mnemonic/internal/vector/sqlitevec"
|
||||
"github.com/lukaszraczylo/claude-mnemonic/pkg/models"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -19,15 +23,41 @@ type Server struct {
|
||||
version string
|
||||
stdin io.Reader
|
||||
stdout io.Writer
|
||||
|
||||
// Store dependencies for enhanced tools
|
||||
observationStore *gorm.ObservationStore
|
||||
patternStore *gorm.PatternStore
|
||||
relationStore *gorm.RelationStore
|
||||
sessionStore *gorm.SessionStore
|
||||
vectorClient *sqlitevec.Client
|
||||
scoreCalculator *scoring.Calculator
|
||||
recalculator *scoring.Recalculator
|
||||
}
|
||||
|
||||
// NewServer creates a new MCP server.
|
||||
func NewServer(searchMgr *search.Manager, version string) *Server {
|
||||
func NewServer(
|
||||
searchMgr *search.Manager,
|
||||
version string,
|
||||
observationStore *gorm.ObservationStore,
|
||||
patternStore *gorm.PatternStore,
|
||||
relationStore *gorm.RelationStore,
|
||||
sessionStore *gorm.SessionStore,
|
||||
vectorClient *sqlitevec.Client,
|
||||
scoreCalculator *scoring.Calculator,
|
||||
recalculator *scoring.Recalculator,
|
||||
) *Server {
|
||||
return &Server{
|
||||
searchMgr: searchMgr,
|
||||
version: version,
|
||||
stdin: os.Stdin,
|
||||
stdout: os.Stdout,
|
||||
searchMgr: searchMgr,
|
||||
version: version,
|
||||
stdin: os.Stdin,
|
||||
stdout: os.Stdout,
|
||||
observationStore: observationStore,
|
||||
patternStore: patternStore,
|
||||
relationStore: relationStore,
|
||||
sessionStore: sessionStore,
|
||||
vectorClient: vectorClient,
|
||||
scoreCalculator: scoreCalculator,
|
||||
recalculator: recalculator,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,6 +363,19 @@ func (s *Server) handleToolsList(req *Request) *Response {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "find_related_observations",
|
||||
Description: "Find observations related to a given observation ID filtered by confidence threshold. Returns related observations sorted by confidence score. Useful for discovering relevant context.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"id"},
|
||||
"properties": map[string]any{
|
||||
"id": map[string]any{"type": "number", "description": "Observation ID"},
|
||||
"min_confidence": map[string]any{"type": "number", "default": 0.5, "minimum": 0.0, "maximum": 1.0, "description": "Minimum confidence threshold"},
|
||||
"limit": map[string]any{"type": "number", "default": 20, "minimum": 1, "maximum": 100},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return &Response{
|
||||
@@ -388,6 +431,12 @@ func (s *Server) handleToolsCall(ctx context.Context, req *Request) *Response {
|
||||
|
||||
// callTool dispatches to the appropriate tool handler.
|
||||
func (s *Server) callTool(ctx context.Context, name string, args json.RawMessage) (string, error) {
|
||||
// Relation discovery tool
|
||||
if name == "find_related_observations" {
|
||||
return s.handleFindRelatedObservations(ctx, args)
|
||||
}
|
||||
|
||||
// Original search-based tools
|
||||
var params search.SearchParams
|
||||
if err := json.Unmarshal(args, ¶ms); err != nil {
|
||||
return "", fmt.Errorf("invalid arguments: %w", err)
|
||||
@@ -537,6 +586,72 @@ func (s *Server) handleTimelineByQuery(ctx context.Context, args json.RawMessage
|
||||
return s.handleTimeline(ctx, args)
|
||||
}
|
||||
|
||||
// handleFindRelatedObservations finds observations related to a given observation ID.
|
||||
func (s *Server) handleFindRelatedObservations(ctx context.Context, args json.RawMessage) (string, error) {
|
||||
var params struct {
|
||||
ID int64 `json:"id"`
|
||||
MinConfidence float64 `json:"min_confidence"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
if err := json.Unmarshal(args, ¶ms); err != nil {
|
||||
return "", fmt.Errorf("invalid arguments: %w", err)
|
||||
}
|
||||
|
||||
if params.ID == 0 {
|
||||
return "", fmt.Errorf("id is required")
|
||||
}
|
||||
|
||||
if params.MinConfidence == 0 {
|
||||
params.MinConfidence = 0.5
|
||||
}
|
||||
|
||||
if params.Limit == 0 {
|
||||
params.Limit = 20
|
||||
}
|
||||
if params.Limit > 100 {
|
||||
params.Limit = 100
|
||||
}
|
||||
|
||||
// Get related observation IDs with confidence filter
|
||||
relatedIDs, err := s.relationStore.GetRelatedObservationIDs(ctx, params.ID, params.MinConfidence)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get related observations: %w", err)
|
||||
}
|
||||
|
||||
if relatedIDs == nil {
|
||||
relatedIDs = []int64{}
|
||||
}
|
||||
|
||||
// Limit results
|
||||
if len(relatedIDs) > params.Limit {
|
||||
relatedIDs = relatedIDs[:params.Limit]
|
||||
}
|
||||
|
||||
// Fetch full observations
|
||||
observations := make([]*models.Observation, 0, len(relatedIDs))
|
||||
for _, id := range relatedIDs {
|
||||
obs, err := s.observationStore.GetObservationByID(ctx, id)
|
||||
if err != nil {
|
||||
continue // Skip errors for individual observations
|
||||
}
|
||||
if obs != nil {
|
||||
observations = append(observations, obs)
|
||||
}
|
||||
}
|
||||
|
||||
response := map[string]any{
|
||||
"observations": observations,
|
||||
"count": len(observations),
|
||||
}
|
||||
|
||||
output, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshal response: %w", err)
|
||||
}
|
||||
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
// sendResponse sends a JSON-RPC response.
|
||||
func (s *Server) sendResponse(resp *Response) {
|
||||
data, err := json.Marshal(resp)
|
||||
|
||||
+20
-20
@@ -24,7 +24,7 @@ func TestServerSuite(t *testing.T) {
|
||||
|
||||
// TestNewServer tests server creation.
|
||||
func (s *ServerSuite) TestNewServer() {
|
||||
server := NewServer(nil, "1.0.0")
|
||||
server := NewServer(nil, "1.0.0", nil, nil, nil, nil, nil, nil, nil)
|
||||
s.NotNil(server)
|
||||
s.Nil(server.searchMgr)
|
||||
s.Equal("1.0.0", server.version)
|
||||
@@ -293,7 +293,7 @@ func TestTimelineParams(t *testing.T) {
|
||||
|
||||
// TestHandleInitialize tests the initialize handler.
|
||||
func TestHandleInitialize(t *testing.T) {
|
||||
server := NewServer(nil, "1.2.3")
|
||||
server := NewServer(nil, "1.2.3", nil, nil, nil, nil, nil, nil, nil)
|
||||
|
||||
req := &Request{
|
||||
JSONRPC: "2.0",
|
||||
@@ -320,7 +320,7 @@ func TestHandleInitialize(t *testing.T) {
|
||||
|
||||
// TestHandleToolsList tests the tools/list handler.
|
||||
func TestHandleToolsList(t *testing.T) {
|
||||
server := NewServer(nil, "1.0.0")
|
||||
server := NewServer(nil, "1.0.0", nil, nil, nil, nil, nil, nil, nil)
|
||||
|
||||
req := &Request{
|
||||
JSONRPC: "2.0",
|
||||
@@ -361,7 +361,7 @@ func TestHandleToolsList(t *testing.T) {
|
||||
|
||||
// TestHandleRequest tests request routing.
|
||||
func TestHandleRequest(t *testing.T) {
|
||||
server := NewServer(nil, "1.0.0")
|
||||
server := NewServer(nil, "1.0.0", nil, nil, nil, nil, nil, nil, nil)
|
||||
ctx := context.Background()
|
||||
|
||||
tests := []struct {
|
||||
@@ -423,7 +423,7 @@ func TestHandleRequest(t *testing.T) {
|
||||
|
||||
// TestHandleToolsCall_InvalidParams tests tools/call with invalid params.
|
||||
func TestHandleToolsCall_InvalidParams(t *testing.T) {
|
||||
server := NewServer(nil, "1.0.0")
|
||||
server := NewServer(nil, "1.0.0", nil, nil, nil, nil, nil, nil, nil)
|
||||
ctx := context.Background()
|
||||
|
||||
req := &Request{
|
||||
@@ -442,7 +442,7 @@ func TestHandleToolsCall_InvalidParams(t *testing.T) {
|
||||
|
||||
// TestCallTool_UnknownTool tests callTool with unknown tool name.
|
||||
func TestCallTool_UnknownTool(t *testing.T) {
|
||||
server := NewServer(nil, "1.0.0")
|
||||
server := NewServer(nil, "1.0.0", nil, nil, nil, nil, nil, nil, nil)
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := server.callTool(ctx, "nonexistent_tool", json.RawMessage(`{}`))
|
||||
@@ -452,7 +452,7 @@ func TestCallTool_UnknownTool(t *testing.T) {
|
||||
|
||||
// TestCallTool_InvalidArgs tests callTool with invalid arguments.
|
||||
func TestCallTool_InvalidArgs(t *testing.T) {
|
||||
server := NewServer(nil, "1.0.0")
|
||||
server := NewServer(nil, "1.0.0", nil, nil, nil, nil, nil, nil, nil)
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := server.callTool(ctx, "search", json.RawMessage(`invalid json`))
|
||||
@@ -574,7 +574,7 @@ func TestJSONRPCErrorCodes(t *testing.T) {
|
||||
|
||||
// TestToolListContainsExpectedSchemas tests that tool schemas are valid.
|
||||
func TestToolListContainsExpectedSchemas(t *testing.T) {
|
||||
server := NewServer(nil, "1.0.0")
|
||||
server := NewServer(nil, "1.0.0", nil, nil, nil, nil, nil, nil, nil)
|
||||
|
||||
req := &Request{
|
||||
JSONRPC: "2.0",
|
||||
@@ -600,7 +600,7 @@ func TestToolListContainsExpectedSchemas(t *testing.T) {
|
||||
|
||||
// TestHandleToolsCall_UnknownTool tests tools/call with unknown tool name.
|
||||
func TestHandleToolsCall_UnknownTool(t *testing.T) {
|
||||
server := NewServer(nil, "1.0.0")
|
||||
server := NewServer(nil, "1.0.0", nil, nil, nil, nil, nil, nil, nil)
|
||||
ctx := context.Background()
|
||||
|
||||
req := &Request{
|
||||
@@ -620,7 +620,7 @@ func TestHandleToolsCall_UnknownTool(t *testing.T) {
|
||||
func TestCallTool_ToolNameRecognition(t *testing.T) {
|
||||
// Note: This test verifies tool routing logic, not execution (which requires searchMgr)
|
||||
// All valid tool names should be in the handleToolsList response
|
||||
server := NewServer(nil, "1.0.0")
|
||||
server := NewServer(nil, "1.0.0", nil, nil, nil, nil, nil, nil, nil)
|
||||
|
||||
req := &Request{
|
||||
JSONRPC: "2.0",
|
||||
@@ -782,7 +782,7 @@ func TestResponseIDTypes(t *testing.T) {
|
||||
|
||||
// TestHandleTimelineByQuery_EmptyQuery tests timeline by query with empty query.
|
||||
func TestHandleTimelineByQuery_EmptyQuery(t *testing.T) {
|
||||
server := NewServer(nil, "1.0.0")
|
||||
server := NewServer(nil, "1.0.0", nil, nil, nil, nil, nil, nil, nil)
|
||||
ctx := context.Background()
|
||||
|
||||
// Empty query should error
|
||||
@@ -793,7 +793,7 @@ func TestHandleTimelineByQuery_EmptyQuery(t *testing.T) {
|
||||
|
||||
// TestHandleTimeline_InvalidJSON tests timeline with invalid JSON.
|
||||
func TestHandleTimeline_InvalidJSON(t *testing.T) {
|
||||
server := NewServer(nil, "1.0.0")
|
||||
server := NewServer(nil, "1.0.0", nil, nil, nil, nil, nil, nil, nil)
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := server.handleTimeline(ctx, json.RawMessage(`{invalid`))
|
||||
@@ -803,7 +803,7 @@ func TestHandleTimeline_InvalidJSON(t *testing.T) {
|
||||
|
||||
// TestHandleTimelineByQuery_InvalidJSON tests timeline by query with invalid JSON.
|
||||
func TestHandleTimelineByQuery_InvalidJSON(t *testing.T) {
|
||||
server := NewServer(nil, "1.0.0")
|
||||
server := NewServer(nil, "1.0.0", nil, nil, nil, nil, nil, nil, nil)
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := server.handleTimelineByQuery(ctx, json.RawMessage(`{invalid`))
|
||||
@@ -813,7 +813,7 @@ func TestHandleTimelineByQuery_InvalidJSON(t *testing.T) {
|
||||
|
||||
// TestHandleTimeline_NoAnchorNoQuery tests timeline with no anchor and no query.
|
||||
func TestHandleTimeline_NoAnchorNoQuery(t *testing.T) {
|
||||
server := NewServer(nil, "1.0.0")
|
||||
server := NewServer(nil, "1.0.0", nil, nil, nil, nil, nil, nil, nil)
|
||||
ctx := context.Background()
|
||||
|
||||
// No anchor_id and no query should return empty result
|
||||
@@ -825,7 +825,7 @@ func TestHandleTimeline_NoAnchorNoQuery(t *testing.T) {
|
||||
|
||||
// TestHandleTimeline_WithDefaults tests timeline default values are applied.
|
||||
func TestHandleTimeline_WithDefaults(t *testing.T) {
|
||||
server := NewServer(nil, "1.0.0")
|
||||
server := NewServer(nil, "1.0.0", nil, nil, nil, nil, nil, nil, nil)
|
||||
ctx := context.Background()
|
||||
|
||||
// With anchor_id but no before/after, defaults should be applied
|
||||
@@ -839,7 +839,7 @@ func TestHandleTimeline_WithDefaults(t *testing.T) {
|
||||
|
||||
// TestServerFields tests Server struct fields.
|
||||
func TestServerFields(t *testing.T) {
|
||||
server := NewServer(nil, "2.0.0")
|
||||
server := NewServer(nil, "2.0.0", nil, nil, nil, nil, nil, nil, nil)
|
||||
|
||||
assert.Equal(t, "2.0.0", server.version)
|
||||
assert.Nil(t, server.searchMgr)
|
||||
@@ -891,7 +891,7 @@ func TestErrorWithNilData(t *testing.T) {
|
||||
|
||||
// TestToolInputSchema tests that tool input schemas have required fields.
|
||||
func TestToolInputSchema(t *testing.T) {
|
||||
server := NewServer(nil, "1.0.0")
|
||||
server := NewServer(nil, "1.0.0", nil, nil, nil, nil, nil, nil, nil)
|
||||
|
||||
req := &Request{
|
||||
JSONRPC: "2.0",
|
||||
@@ -960,7 +960,7 @@ func TestToolCallParamsWithComplexArgs(t *testing.T) {
|
||||
|
||||
// TestCallTool_UnknownToolName tests callTool with various unknown tool names.
|
||||
func TestCallTool_UnknownToolName(t *testing.T) {
|
||||
server := NewServer(nil, "1.0.0")
|
||||
server := NewServer(nil, "1.0.0", nil, nil, nil, nil, nil, nil, nil)
|
||||
ctx := context.Background()
|
||||
|
||||
unknownTools := []string{
|
||||
@@ -1009,7 +1009,7 @@ func TestTimelineParams_Validation(t *testing.T) {
|
||||
|
||||
// TestHandleToolsCall_UnknownToolNameError tests tools/call with unknown tool returns error.
|
||||
func TestHandleToolsCall_UnknownToolNameError(t *testing.T) {
|
||||
server := NewServer(nil, "1.0.0")
|
||||
server := NewServer(nil, "1.0.0", nil, nil, nil, nil, nil, nil, nil)
|
||||
ctx := context.Background()
|
||||
|
||||
req := &Request{
|
||||
@@ -1031,7 +1031,7 @@ func TestHandleToolsCall_UnknownToolNameError(t *testing.T) {
|
||||
|
||||
// TestHandleToolsCall_EmptyParams tests tools/call with empty params.
|
||||
func TestHandleToolsCall_EmptyParams(t *testing.T) {
|
||||
server := NewServer(nil, "1.0.0")
|
||||
server := NewServer(nil, "1.0.0", nil, nil, nil, nil, nil, nil, nil)
|
||||
ctx := context.Background()
|
||||
|
||||
req := &Request{
|
||||
|
||||
Reference in New Issue
Block a user