forgejo-mcp/operation/resource/bound.go
Christoph Görn 4249374bfe
feat: ✨ MCP resource templates — slices 1+2 (core framework + commit)
Implements the first two slices of the OpenSpec change
mcp-resource-templates (merged via PR #148):

Slice 1 — mcp-resources-core
- server.WithResourceCapabilities(false, false) enabled per design.md D5
- new operation/resource/ package with:
  - parse.go: typed URI parsers for owner/repo/commit/issue/pr/comment/status
    enforcing 40-hex sha, numeric index, kind ∈ {issue, pr}
  - bound.go: EmbeddedListCap = 30 + generic Bounded[T] helper with sentinel
  - errors.go: MapForgejoError maps 403 → -32002, 404 → -32003
  - register.go: thin AddTemplate wrapper over mcp.NewResourceTemplate
- RegisterCoreResources stub wired from Run()

Slice 2 — mcp-resource-commit
- operation/repo/resources_commit.go registers template
  forgejo://repo/{owner}/{repo}/commit/{sha}
- Handler returns application/json metadata + text/markdown sidecar (D9)
- Short-sha rejected at parse layer; SDK 404 surfaces via MapForgejoError
- RegisterCommitResource wired from RegisterCoreResources
- README "Resources" section added with one row for commit

Tests
- 8 new test files across operation/resource/ and operation/repo/
- All passing under go test -count=1 ./...

Tasks
- 17/47 checkboxes ticked in
  openspec/changes/mcp-resource-templates/tasks.md (slice 1+2;
  1.11 manual client check deferred)

Driven by team:dev-loop. Closes the slice 1+2 portion of forgejo-mcp-13x.
2026-05-28 14:31:55 +02:00

41 lines
1.1 KiB
Go

package resource
import "fmt"
// EmbeddedListCap is the default maximum number of items embedded in a resource response.
const EmbeddedListCap = 30
// BoundedResult holds a capped slice of items and metadata for sentinel generation.
type BoundedResult struct {
Items []string
Total int
Cap int
ListTool string
Truncated bool
}
// Bounded returns a BoundedResult capping items at cap (use EmbeddedListCap for default).
// listTool names the tool the caller should use to fetch more items.
func Bounded(items []string, cap int, listTool string) BoundedResult {
total := len(items)
truncated := total > cap
shown := items
if truncated {
shown = items[:cap]
}
return BoundedResult{
Items: shown,
Total: total,
Cap: cap,
ListTool: listTool,
Truncated: truncated,
}
}
// Sentinel returns a truncation marker string when the list was truncated, or empty string otherwise.
func (b BoundedResult) Sentinel() string {
if !b.Truncated {
return ""
}
return fmt.Sprintf("[truncated: %d of %d items shown. Use %s tool to fetch more.]", b.Cap, b.Total, b.ListTool)
}