Ctrl K
ComunidadGuías…

internal/mcptools/get_updates.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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))
}