internal/mcptools/get_updates.go
Rama main · réplica de lectura del repositorio original.
package mcptools
import (
"context"
"errors"
"log"
"time"
"xprem/internal/repository"
"xprem/internal/types"
mcpprot "github.com/modelcontextprotocol/go-sdk/mcp"
)
const (
defaultUpdatesLimit = 20
maxUpdatesLimit = 50
)
type GetUpdatesInput struct {
AppId string `json:"appId" jsonschema:"the app id, as returned by get_apps"`
Branch string `json:"branch,omitempty" jsonschema:"filter by branch name, as returned by get_branches"`
RuntimeVersion string `json:"runtimeVersion,omitempty" jsonschema:"filter by runtime version, as returned by get_runtime_versions"`
Platform string `json:"platform,omitempty" jsonschema:"filter by platform: ios or android"`
UpdateUUID string `json:"updateUUID,omitempty" jsonschema:"filter by updateUUID; matches any update whose uuid contains this value, case-insensitive"`
PublishGroup string `json:"publishGroup,omitempty" jsonschema:"filter by publish group id (the updates published together); matches by substring, case-insensitive"`
CommitHash string `json:"commitHash,omitempty" jsonschema:"filter by the git commit hash the update was built from; matches by substring, case-insensitive"`
From string `json:"from,omitempty" jsonschema:"only updates published at or after this RFC3339 timestamp"`
To string `json:"to,omitempty" jsonschema:"only updates published at or before this RFC3339 timestamp"`
Limit int `json:"limit,omitempty" jsonschema:"maximum updates returned; default 20, max 50; prefer narrowing with filters over raising it"`
Cursor string `json:"cursor,omitempty" jsonschema:"page forward: pass the nextCursor of a previous answer to fetch older updates"`
}
type GetUpdatesOutput struct {
Updates []types.UpdateFeedItem `json:"updates"`
// NextCursor is set when more updates exist past this page.
NextCursor string `json:"nextCursor,omitempty" jsonschema:"present when more updates exist; pass it back as cursor to fetch the next page"`
}
func getUpdatesHandler(deps Deps) func(ctx context.Context, req *mcpprot.CallToolRequest, input GetUpdatesInput) (*mcpprot.CallToolResult, GetUpdatesOutput, error) {
return func(ctx context.Context, req *mcpprot.CallToolRequest, input GetUpdatesInput) (*mcpprot.CallToolResult, GetUpdatesOutput, error) {
principal := PrincipalFromRequest(req)
if principal == nil {
return nil, GetUpdatesOutput{}, errors.New("no authenticated account on this session")
}
if err := requireAppVisible(ctx, deps, principal, input.AppId); err != nil {
return nil, GetUpdatesOutput{}, err
}
var platform types.Platform
if input.Platform != "" {
parsed, err := types.ParsePlatform(input.Platform)
if err != nil {
return nil, GetUpdatesOutput{}, errors.New("platform must be ios or android")
}
platform = parsed
}
limit := input.Limit
if limit <= 0 {
limit = defaultUpdatesLimit
}
if limit > maxUpdatesLimit {
limit = maxUpdatesLimit
}
// One extra row answers "is there a next page"; it is never returned.
query := types.UpdateFeedQuery{
Branch: input.Branch,
RuntimeVersion: input.RuntimeVersion,
Platform: platform,
UpdateUUID: input.UpdateUUID,
PublishGroup: input.PublishGroup,
CommitHash: input.CommitHash,
Limit: limit + 1,
}
cursor, err := types.DecodeUpdateFeedCursor(input.Cursor)
if err != nil {
return nil, GetUpdatesOutput{}, errors.New("cursor is invalid; pass a nextCursor from a previous answer")
}
if cursor != nil {
query.CursorCreatedAt = &cursor.CreatedAt
query.CursorBranchID = cursor.BranchID
query.CursorUpdateID = cursor.UpdateID
}
for _, bound := range []struct {
raw string
target **time.Time
}{{input.From, &query.From}, {input.To, &query.To}} {
if bound.raw == "" {
continue
}
parsed, err := time.Parse(time.RFC3339, bound.raw)
if err != nil {
return nil, GetUpdatesOutput{}, errors.New("from and to must be RFC3339 timestamps")
}
*bound.target = &parsed
}
updates, err := deps.UpdateFeed.GetUpdateFeed(ctx, input.AppId, query)
if err != nil {
if errors.Is(err, repository.ErrNotSupportedInStatelessMode) {
return nil, GetUpdatesOutput{}, errors.New("the update feed requires the control plane; this deployment runs in stateless mode")
}
log.Printf("mcp get_updates failed for app %s: %v", input.AppId, err)
return nil, GetUpdatesOutput{}, errors.New("could not list the updates, try again later")
}
if updates == nil {
updates = []types.UpdateFeedItem{}
}
output := GetUpdatesOutput{Updates: updates}
if len(updates) > limit {
output.Updates = updates[:limit]
cursor, err := types.EncodeUpdateFeedCursor(output.Updates[len(output.Updates)-1])
if err != nil {
// The page is valid; only paging past it is not. Answer it
// without a cursor rather than failing the call.
log.Printf("mcp get_updates could not encode the cursor for app %s: %v", input.AppId, err)
}
output.NextCursor = cursor
}
return nil, output, nil
}
}
func registerGetUpdates(server *mcpprot.Server, deps Deps) {
mcpprot.AddTool(server, &mcpprot.Tool{
Name: "get_updates",
Description: "The published updates of an app (appId required), newest first, max 50 per call. Narrow with branch, runtimeVersion, platform, updateUUID, publishGroup, commitHash, or a from/to date range; when nextCursor is present, pass it back as cursor for the next page.",
Annotations: &mcpprot.ToolAnnotations{Title: "List updates", ReadOnlyHint: true},
}, getUpdatesHandler(deps))
}