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 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{}, err
}
arc, _, resolved, err := archiveAt(ctx, b, sp, rev)
if err != nil {
return listOutput{}, err
}
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 {
return listOutput{}, err
}
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
}