| 1 |
|
package mcpsrv |
| 2 |
|
|
| 3 |
|
import ( |
| 4 |
|
"context" |
| 5 |
|
"errors" |
| 6 |
|
"strings" |
| 7 |
|
) |
| 8 |
|
|
| 9 |
|
type listInput struct { |
| 10 |
|
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."` |
| 11 |
|
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."` |
| 12 |
|
} |
| 13 |
|
|
| 14 |
|
type spaceEntry struct { |
| 15 |
|
// Space is the name every other tool takes: "~owner/name". |
| 16 |
|
Space string `json:"space"` |
| 17 |
|
Owner string `json:"owner"` |
| 18 |
|
Name string `json:"name"` |
| 19 |
|
} |
| 20 |
|
|
| 21 |
|
type documentEntry struct { |
| 22 |
|
// ID is how spec_read addresses this document. |
| 23 |
|
ID string `json:"id"` |
| 24 |
|
DocID string `json:"doc_id,omitempty"` |
| 25 |
|
Path string `json:"path"` |
| 26 |
|
Blob string `json:"blob,omitempty"` |
| 27 |
|
Title string `json:"title,omitempty"` |
| 28 |
|
// Section is the top-level directory the document lives under, and is what |
| 29 |
|
// spec_search's sections filter takes. |
| 30 |
|
Section string `json:"section,omitempty"` |
| 31 |
|
// Status is the authored lifecycle marker, not approval state. |
| 32 |
|
Status string `json:"status,omitempty"` |
| 33 |
|
Summary string `json:"summary,omitempty"` |
| 34 |
|
Tags []string `json:"tags,omitempty"` |
| 35 |
|
} |
| 36 |
|
|
| 37 |
|
// listOutput carries whichever of the two listings was asked for. One tool |
| 38 |
|
// rather than two because the answer to "what can I read" is one question with |
| 39 |
|
// a drill-down, and an agent that has just been handed a space name should not |
| 40 |
|
// have to find a second tool to use it. |
| 41 |
|
type listOutput struct { |
| 42 |
|
// Spaces is set when no space was named. |
| 43 |
|
Spaces []spaceEntry `json:"spaces,omitempty"` |
| 44 |
|
// Space, Rev and Documents are set when one was. |
| 45 |
|
Space string `json:"space,omitempty"` |
| 46 |
|
// Rev is the revision listed, resolved to an immutable commit. Pass it to |
| 47 |
|
// spec_read to read any of these documents at exactly this revision. |
| 48 |
|
Rev string `json:"rev,omitempty"` |
| 49 |
|
Documents []documentEntry `json:"documents,omitempty"` |
| 50 |
|
} |
| 51 |
|
|
| 52 |
6 |
func listHandler(ctx context.Context, b Backend, in listInput) (listOutput, error) { |
| 53 |
6 |
if err := requireRead(ctx); err != nil { |
| 54 |
1 |
return listOutput{}, err |
| 55 |
1 |
} |
| 56 |
5 |
if strings.TrimSpace(in.Space) == "" { |
| 57 |
2 |
// A rev with no space is a caller that meant to name one. Ignoring it |
| 58 |
2 |
// would answer a different question than was asked and look like it |
| 59 |
2 |
// had worked. |
| 60 |
2 |
if strings.TrimSpace(in.Rev) != "" { |
| 61 |
1 |
return listOutput{}, errors.New("rev names a revision of a space; pass space as well, or omit rev to list spaces") |
| 62 |
1 |
} |
| 63 |
1 |
return listSpaces(ctx, b) |
| 64 |
|
} |
| 65 |
|
|
| 66 |
3 |
ref, err := parseSpace(in.Space) |
| 67 |
3 |
if err != nil { |
| 68 |
0 |
return listOutput{}, err |
| 69 |
0 |
} |
| 70 |
3 |
rev, err := parseRev(in.Rev) |
| 71 |
3 |
if err != nil { |
| 72 |
1 |
return listOutput{}, err |
| 73 |
1 |
} |
| 74 |
2 |
sp, err := b.Docs.OpenSpace(ctx, ref) |
| 75 |
2 |
if err != nil { |
| 76 |
0 |
return listOutput{}, missingOrDenied(err, "spec_list", noSpace(ref)) |
| 77 |
0 |
} |
| 78 |
2 |
arc, _, resolved, err := archiveAt(ctx, b, sp, rev) |
| 79 |
2 |
if err != nil { |
| 80 |
0 |
return listOutput{}, missingOrDenied(err, "spec_list", noRevision(ref, rev)) |
| 81 |
0 |
} |
| 82 |
|
|
| 83 |
2 |
out := listOutput{Space: ref.String(), Rev: resolved, Documents: make([]documentEntry, 0, len(arc.All()))} |
| 84 |
3 |
for _, p := range arc.All() { |
| 85 |
3 |
out.Documents = append(out.Documents, documentEntry{ |
| 86 |
3 |
ID: p.ID, |
| 87 |
3 |
DocID: p.DocID, |
| 88 |
3 |
Path: p.Path, |
| 89 |
3 |
Blob: p.Blob, |
| 90 |
3 |
Title: p.Title, |
| 91 |
3 |
Section: p.Section, |
| 92 |
3 |
Status: string(p.Status), |
| 93 |
3 |
Summary: p.Summary, |
| 94 |
3 |
Tags: p.Tags, |
| 95 |
3 |
}) |
| 96 |
3 |
} |
| 97 |
2 |
return out, nil |
| 98 |
|
} |
| 99 |
|
|
| 100 |
1 |
func listSpaces(ctx context.Context, b Backend) (listOutput, error) { |
| 101 |
1 |
spaces, err := b.Docs.ListSpaces(ctx) |
| 102 |
1 |
if err != nil { |
| 103 |
0 |
// No space was named, so there is no "that one does not exist" to |
| 104 |
0 |
// answer: whatever went wrong here is this service's. |
| 105 |
0 |
return listOutput{}, internalError(err, "spec_list") |
| 106 |
0 |
} |
| 107 |
1 |
out := listOutput{Spaces: make([]spaceEntry, 0, len(spaces))} |
| 108 |
2 |
for _, sp := range spaces { |
| 109 |
2 |
out.Spaces = append(out.Spaces, spaceEntry{ |
| 110 |
2 |
Space: sp.Ref.String(), |
| 111 |
2 |
Owner: sp.Ref.Owner, |
| 112 |
2 |
Name: sp.Ref.Name, |
| 113 |
2 |
}) |
| 114 |
2 |
} |
| 115 |
1 |
return out, nil |
| 116 |
|
} |