Skip to content

Commit 9ae9acf

Browse files
fix(mcp): skip project filter for scope=personal with no explicit project (#425)
When scope=personal is passed to handleSearch or handleContext without an explicit project argument, the cwd-detected project was still applied as a store-level filter, hiding personal memories from other projects. Clear the project filter before calling s.Search / s.FormatContext whenever scope=personal and no per-call project override is present. The envelope project (used for UI context) is preserved unchanged. Closes #391
1 parent 2087c5b commit 9ae9acf

2 files changed

Lines changed: 147 additions & 2 deletions

File tree

internal/mcp/mcp.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -933,12 +933,20 @@ func handleSearch(s *store.Store, cfg MCPConfig, activity *SessionActivity) serv
933933
detRes.Project = project // JR2-1: keep envelope in sync with normalized query project
934934
}
935935

936+
// REQ-391: personal scope is cross-project by definition. When scope=personal
937+
// and no explicit project override was provided, clear the project filter so
938+
// memories from all projects are visible (not just the cwd-detected one).
939+
searchProject := project
940+
if scope == "personal" && strings.TrimSpace(projectOverride) == "" {
941+
searchProject = ""
942+
}
943+
936944
sessionID := defaultSessionID(project)
937945
activity.RecordToolCall(sessionID)
938946

939947
results, err := s.Search(query, store.SearchOptions{
940948
Type: typ,
941-
Project: project,
949+
Project: searchProject,
942950
Scope: scope,
943951
Limit: limit,
944952
})
@@ -1387,10 +1395,18 @@ func handleContext(s *store.Store, cfg MCPConfig, activity *SessionActivity) ser
13871395
project, _ = store.NormalizeProject(project)
13881396
detRes.Project = project // JR2-1: keep envelope in sync with normalized query project
13891397

1398+
// REQ-391: personal scope is cross-project by definition. When scope=personal
1399+
// and no explicit project override was provided, clear the project filter so
1400+
// observations from all projects are returned (not just the cwd-detected one).
1401+
contextProject := project
1402+
if scope == "personal" && strings.TrimSpace(projectOverride) == "" {
1403+
contextProject = ""
1404+
}
1405+
13901406
sessionID := defaultSessionID(project)
13911407
activity.RecordToolCall(sessionID)
13921408

1393-
contextResult, err := s.FormatContext(project, scope)
1409+
contextResult, err := s.FormatContext(contextProject, scope)
13941410
if err != nil {
13951411
return mcp.NewToolResultError("Failed to get context: " + err.Error()), nil
13961412
}

internal/mcp/mcp_test.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6520,6 +6520,135 @@ func TestProcessOverrideSaveHandlerWritesToDefaultProject(t *testing.T) {
65206520
}
65216521
}
65226522

6523+
// TestHandleSearchPersonalScopeIgnoresCWDProject verifies that when scope=personal
6524+
// and no explicit project is given, handleSearch returns personal memories from
6525+
// ALL projects rather than filtering to the cwd-detected project (issue #391).
6526+
func TestHandleSearchPersonalScopeIgnoresCWDProject(t *testing.T) {
6527+
s := newMCPTestStore(t)
6528+
6529+
// Create sessions and personal observations in two distinct projects.
6530+
if err := s.CreateSession("sess-proj-a", "project-alpha", "/tmp/project-alpha"); err != nil {
6531+
t.Fatalf("create session project-alpha: %v", err)
6532+
}
6533+
if err := s.CreateSession("sess-proj-b", "project-beta", "/tmp/project-beta"); err != nil {
6534+
t.Fatalf("create session project-beta: %v", err)
6535+
}
6536+
6537+
_, err := s.AddObservation(store.AddObservationParams{
6538+
SessionID: "sess-proj-a",
6539+
Type: "decision",
6540+
Title: "personal cross-project preference",
6541+
Content: "always use structured logging",
6542+
Project: "project-alpha",
6543+
Scope: "personal",
6544+
})
6545+
if err != nil {
6546+
t.Fatalf("add personal observation project-alpha: %v", err)
6547+
}
6548+
6549+
_, err = s.AddObservation(store.AddObservationParams{
6550+
SessionID: "sess-proj-b",
6551+
Type: "decision",
6552+
Title: "personal note from beta",
6553+
Content: "prefer context-based cancellation",
6554+
Project: "project-beta",
6555+
Scope: "personal",
6556+
})
6557+
if err != nil {
6558+
t.Fatalf("add personal observation project-beta: %v", err)
6559+
}
6560+
6561+
// Simulate cwd being project-alpha's directory; the handler should NOT filter
6562+
// results to project-alpha when scope=personal is requested without an explicit project.
6563+
dir := t.TempDir()
6564+
t.Chdir(dir)
6565+
6566+
h := handleSearch(s, MCPConfig{}, NewSessionActivity(10*time.Minute))
6567+
res, err := h(context.Background(), mcppkg.CallToolRequest{Params: mcppkg.CallToolParams{Arguments: map[string]any{
6568+
"query": "personal",
6569+
"scope": "personal",
6570+
// no "project" argument — must NOT default to cwd project
6571+
}}})
6572+
if err != nil {
6573+
t.Fatalf("search handler error: %v", err)
6574+
}
6575+
if res.IsError {
6576+
t.Fatalf("unexpected error: %s", callResultText(t, res))
6577+
}
6578+
6579+
text := callResultText(t, res)
6580+
// Both personal memories must be visible regardless of cwd project.
6581+
if !strings.Contains(text, "personal cross-project preference") {
6582+
t.Errorf("expected personal memory from project-alpha in results; got: %s", text)
6583+
}
6584+
if !strings.Contains(text, "personal note from beta") {
6585+
t.Errorf("expected personal memory from project-beta in results; got: %s", text)
6586+
}
6587+
}
6588+
6589+
// TestHandleContextPersonalScopeIgnoresCWDProject verifies that when scope=personal
6590+
// and no explicit project is given, handleContext returns personal observations from
6591+
// ALL projects rather than filtering to the cwd-detected project (issue #391).
6592+
func TestHandleContextPersonalScopeIgnoresCWDProject(t *testing.T) {
6593+
s := newMCPTestStore(t)
6594+
6595+
if err := s.CreateSession("ctx-sess-a", "ctx-alpha", "/tmp/ctx-alpha"); err != nil {
6596+
t.Fatalf("create session ctx-alpha: %v", err)
6597+
}
6598+
if err := s.CreateSession("ctx-sess-b", "ctx-beta", "/tmp/ctx-beta"); err != nil {
6599+
t.Fatalf("create session ctx-beta: %v", err)
6600+
}
6601+
6602+
_, err := s.AddObservation(store.AddObservationParams{
6603+
SessionID: "ctx-sess-a",
6604+
Type: "pattern",
6605+
Title: "personal pattern from alpha",
6606+
Content: "use table-driven tests everywhere",
6607+
Project: "ctx-alpha",
6608+
Scope: "personal",
6609+
})
6610+
if err != nil {
6611+
t.Fatalf("add personal observation ctx-alpha: %v", err)
6612+
}
6613+
6614+
_, err = s.AddObservation(store.AddObservationParams{
6615+
SessionID: "ctx-sess-b",
6616+
Type: "pattern",
6617+
Title: "personal pattern from beta",
6618+
Content: "prefer explicit error wrapping",
6619+
Project: "ctx-beta",
6620+
Scope: "personal",
6621+
})
6622+
if err != nil {
6623+
t.Fatalf("add personal observation ctx-beta: %v", err)
6624+
}
6625+
6626+
// Simulate cwd being ctx-alpha's directory.
6627+
dir := t.TempDir()
6628+
t.Chdir(dir)
6629+
6630+
h := handleContext(s, MCPConfig{}, NewSessionActivity(10*time.Minute))
6631+
res, err := h(context.Background(), mcppkg.CallToolRequest{Params: mcppkg.CallToolParams{Arguments: map[string]any{
6632+
"scope": "personal",
6633+
// no "project" argument — must NOT default to cwd project
6634+
}}})
6635+
if err != nil {
6636+
t.Fatalf("context handler error: %v", err)
6637+
}
6638+
if res.IsError {
6639+
t.Fatalf("unexpected error: %s", callResultText(t, res))
6640+
}
6641+
6642+
text := callResultText(t, res)
6643+
// Both personal observations must appear in the context output.
6644+
if !strings.Contains(text, "personal pattern from alpha") {
6645+
t.Errorf("expected personal memory from ctx-alpha in context; got: %s", text)
6646+
}
6647+
if !strings.Contains(text, "personal pattern from beta") {
6648+
t.Errorf("expected personal memory from ctx-beta in context; got: %s", text)
6649+
}
6650+
}
6651+
65236652
// ─── #403/#413: handleSessionSummary process-override tests ──────────────────
65246653

65256654
// TestSessionSummary_ProcessOverrideWritesToDefaultProject verifies that when

0 commit comments

Comments
 (0)