fix(mcp): snapshot exposed service slices
Return copies from service accessors and ignore nil subsystems during construction to keep the MCP service API stable and AX-friendly. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
aae824a4d0
commit
2a4e8b7ba3
2 changed files with 31 additions and 6 deletions
|
|
@ -83,7 +83,6 @@ func New(opts Options) (*Service, error) {
|
|||
server: server,
|
||||
processService: opts.ProcessService,
|
||||
wsHub: opts.WSHub,
|
||||
subsystems: opts.Subsystems,
|
||||
logger: log.Default(),
|
||||
processMeta: make(map[string]processRuntime),
|
||||
}
|
||||
|
|
@ -115,7 +114,12 @@ func New(opts Options) (*Service, error) {
|
|||
|
||||
s.registerTools(s.server)
|
||||
|
||||
for _, sub := range s.subsystems {
|
||||
s.subsystems = make([]Subsystem, 0, len(opts.Subsystems))
|
||||
for _, sub := range opts.Subsystems {
|
||||
if sub == nil {
|
||||
continue
|
||||
}
|
||||
s.subsystems = append(s.subsystems, sub)
|
||||
if sn, ok := sub.(SubsystemWithNotifier); ok {
|
||||
sn.SetNotifier(s)
|
||||
}
|
||||
|
|
@ -141,7 +145,7 @@ func New(opts Options) (*Service, error) {
|
|||
// fmt.Println(sub.Name())
|
||||
// }
|
||||
func (s *Service) Subsystems() []Subsystem {
|
||||
return s.subsystems
|
||||
return slices.Clone(s.subsystems)
|
||||
}
|
||||
|
||||
// SubsystemsSeq returns an iterator over the registered subsystems.
|
||||
|
|
@ -150,7 +154,7 @@ func (s *Service) Subsystems() []Subsystem {
|
|||
// fmt.Println(sub.Name())
|
||||
// }
|
||||
func (s *Service) SubsystemsSeq() iter.Seq[Subsystem] {
|
||||
return slices.Values(s.subsystems)
|
||||
return slices.Values(slices.Clone(s.subsystems))
|
||||
}
|
||||
|
||||
// Tools returns all recorded tool metadata.
|
||||
|
|
@ -159,7 +163,7 @@ func (s *Service) SubsystemsSeq() iter.Seq[Subsystem] {
|
|||
// fmt.Printf("%s (%s): %s\n", t.Name, t.Group, t.Description)
|
||||
// }
|
||||
func (s *Service) Tools() []ToolRecord {
|
||||
return s.tools
|
||||
return slices.Clone(s.tools)
|
||||
}
|
||||
|
||||
// ToolsSeq returns an iterator over all recorded tool metadata.
|
||||
|
|
@ -168,7 +172,7 @@ func (s *Service) Tools() []ToolRecord {
|
|||
// fmt.Println(rec.Name)
|
||||
// }
|
||||
func (s *Service) ToolsSeq() iter.Seq[ToolRecord] {
|
||||
return slices.Values(s.tools)
|
||||
return slices.Values(slices.Clone(s.tools))
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down all subsystems that support it.
|
||||
|
|
|
|||
|
|
@ -88,6 +88,27 @@ func TestSubsystem_Good_MultipleSubsystems(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSubsystem_Good_NilEntriesIgnoredAndSnapshots(t *testing.T) {
|
||||
sub := &stubSubsystem{name: "snap-sub"}
|
||||
svc, err := New(Options{Subsystems: []Subsystem{nil, sub}})
|
||||
if err != nil {
|
||||
t.Fatalf("New() failed: %v", err)
|
||||
}
|
||||
|
||||
subs := svc.Subsystems()
|
||||
if len(subs) != 1 {
|
||||
t.Fatalf("expected 1 subsystem after filtering nil entries, got %d", len(subs))
|
||||
}
|
||||
if subs[0].Name() != "snap-sub" {
|
||||
t.Fatalf("expected snap-sub, got %q", subs[0].Name())
|
||||
}
|
||||
|
||||
subs[0] = nil
|
||||
if svc.Subsystems()[0] == nil {
|
||||
t.Fatal("expected Subsystems() to return a snapshot, not the live slice")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubsystem_Good_NotifierSetBeforeRegistration(t *testing.T) {
|
||||
sub := ¬ifierSubsystem{stubSubsystem: stubSubsystem{name: "notifier-sub"}}
|
||||
_, err := New(Options{Subsystems: []Subsystem{sub}})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue