package mcpsrv import ( "context" "errors" "strings" ) type listInput struct { Space string `json:"space,omitempty" jsonschema:"the space to list documents from, written \"~owner/name\". Omit it to list the spaces on this instance instead."` Rev string `json:"rev,omitempty" jsonschema:"pin the listing to one immutable revision, given as a git object name (lowercase hex). Omit to list the space's approved head. Requires space."` } type spaceEntry struct { // Space is the name every other tool takes: "~owner/name". Space string `json:"space"` Owner string `json:"owner"` Name string `json:"name"` } type documentEntry struct { // ID is how spec_read addresses this document. ID string `json:"id"` DocID string `json:"doc_id,omitempty"` Path string `json:"path"` Blob string `json:"blob,omitempty"` Title string `json:"title,omitempty"` // Section is the top-level directory the document lives under, and is what // spec_search's sections filter takes. Section string `json:"section,omitempty"` // Status is the authored lifecycle marker, not approval state. Status string `json:"status,omitempty"` Summary string `json:"summary,omitempty"` Tags []string `json:"tags,omitempty"` } // listOutput carries whichever of the two listings was asked for. One tool // rather than two because the answer to "what can I read" is one question with // a drill-down, and an agent that has just been handed a space name should not // have to find a second tool to use it. type listOutput struct { // Spaces is set when no space was named. Spaces []spaceEntry `json:"spaces,omitempty"` // Space, Rev and Documents are set when one was. Space string `json:"space,omitempty"` // Rev is the revision listed, resolved to an immutable commit. Pass it to // spec_read to read any of these documents at exactly this revision. Rev string `json:"rev,omitempty"` Documents []documentEntry `json:"documents,omitempty"` } func listHandler(ctx context.Context, b Backend, in listInput) (listOutput, error) { if err := requireRead(ctx); err != nil { return listOutput{}, err } if strings.TrimSpace(in.Space) == "" { // A rev with no space is a caller that meant to name one. Ignoring it // would answer a different question than was asked and look like it // had worked. if strings.TrimSpace(in.Rev) != "" { return listOutput{}, errors.New("rev names a revision of a space; pass space as well, or omit rev to list spaces") } return listSpaces(ctx, b) } ref, err := parseSpace(in.Space) if err != nil { return listOutput{}, err } rev, err := parseRev(in.Rev) if err != nil { return listOutput{}, err } sp, err := b.Docs.OpenSpace(ctx, ref) if err != nil { return listOutput{}, missingOrDenied(err, "spec_list", noSpace(ref)) } arc, _, resolved, err := archiveAt(ctx, b, sp, rev) if err != nil { return listOutput{}, missingOrDenied(err, "spec_list", noRevision(ref, rev)) } out := listOutput{Space: ref.String(), Rev: resolved, Documents: make([]documentEntry, 0, len(arc.All()))} for _, p := range arc.All() { out.Documents = append(out.Documents, documentEntry{ ID: p.ID, DocID: p.DocID, Path: p.Path, Blob: p.Blob, Title: p.Title, Section: p.Section, Status: string(p.Status), Summary: p.Summary, Tags: p.Tags, }) } return out, nil } func listSpaces(ctx context.Context, b Backend) (listOutput, error) { spaces, err := b.Docs.ListSpaces(ctx) if err != nil { // No space was named, so there is no "that one does not exist" to // answer: whatever went wrong here is this service's. return listOutput{}, internalError(err, "spec_list") } out := listOutput{Spaces: make([]spaceEntry, 0, len(spaces))} for _, sp := range spaces { out.Spaces = append(out.Spaces, spaceEntry{ Space: sp.Ref.String(), Owner: sp.Ref.Owner, Name: sp.Ref.Name, }) } return out, nil }