381 lines
13 KiB
Go
381 lines
13 KiB
Go
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
package agentic
|
|
|
|
import (
|
|
"context"
|
|
|
|
core "dappco.re/go/core"
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
// sprint := agentic.Sprint{Slug: "ax-follow-up", Title: "AX Follow-up", Status: "active"}
|
|
type Sprint struct {
|
|
ID int `json:"id"`
|
|
WorkspaceID int `json:"workspace_id,omitempty"`
|
|
Slug string `json:"slug"`
|
|
Title string `json:"title"`
|
|
Goal string `json:"goal,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
StartedAt string `json:"started_at,omitempty"`
|
|
EndedAt string `json:"ended_at,omitempty"`
|
|
CreatedAt string `json:"created_at,omitempty"`
|
|
UpdatedAt string `json:"updated_at,omitempty"`
|
|
}
|
|
|
|
// input := agentic.SprintCreateInput{Title: "AX Follow-up", Goal: "Finish RFC parity"}
|
|
type SprintCreateInput struct {
|
|
Title string `json:"title"`
|
|
Goal string `json:"goal,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
StartedAt string `json:"started_at,omitempty"`
|
|
EndedAt string `json:"ended_at,omitempty"`
|
|
}
|
|
|
|
// input := agentic.SprintGetInput{Slug: "ax-follow-up"}
|
|
type SprintGetInput struct {
|
|
ID string `json:"id,omitempty"`
|
|
Slug string `json:"slug,omitempty"`
|
|
}
|
|
|
|
// input := agentic.SprintListInput{Status: "active", Limit: 10}
|
|
type SprintListInput struct {
|
|
Status string `json:"status,omitempty"`
|
|
Limit int `json:"limit,omitempty"`
|
|
}
|
|
|
|
// input := agentic.SprintUpdateInput{Slug: "ax-follow-up", Status: "completed"}
|
|
type SprintUpdateInput struct {
|
|
ID string `json:"id,omitempty"`
|
|
Slug string `json:"slug,omitempty"`
|
|
Title string `json:"title,omitempty"`
|
|
Goal string `json:"goal,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
StartedAt string `json:"started_at,omitempty"`
|
|
EndedAt string `json:"ended_at,omitempty"`
|
|
}
|
|
|
|
// input := agentic.SprintArchiveInput{Slug: "ax-follow-up"}
|
|
type SprintArchiveInput struct {
|
|
ID string `json:"id,omitempty"`
|
|
Slug string `json:"slug,omitempty"`
|
|
}
|
|
|
|
// out := agentic.SprintOutput{Success: true, Sprint: agentic.Sprint{Slug: "ax-follow-up"}}
|
|
type SprintOutput struct {
|
|
Success bool `json:"success"`
|
|
Sprint Sprint `json:"sprint"`
|
|
}
|
|
|
|
// out := agentic.SprintListOutput{Success: true, Count: 1, Sprints: []agentic.Sprint{{Slug: "ax-follow-up"}}}
|
|
type SprintListOutput struct {
|
|
Success bool `json:"success"`
|
|
Count int `json:"count"`
|
|
Sprints []Sprint `json:"sprints"`
|
|
}
|
|
|
|
// out := agentic.SprintArchiveOutput{Success: true, Archived: "ax-follow-up"}
|
|
type SprintArchiveOutput struct {
|
|
Success bool `json:"success"`
|
|
Archived string `json:"archived"`
|
|
}
|
|
|
|
// result := c.Action("sprint.create").Run(ctx, core.NewOptions(core.Option{Key: "title", Value: "AX Follow-up"}))
|
|
func (s *PrepSubsystem) handleSprintCreate(ctx context.Context, options core.Options) core.Result {
|
|
_, output, err := s.sprintCreate(ctx, nil, SprintCreateInput{
|
|
Title: optionStringValue(options, "title"),
|
|
Goal: optionStringValue(options, "goal"),
|
|
Status: optionStringValue(options, "status"),
|
|
Metadata: optionAnyMapValue(options, "metadata"),
|
|
StartedAt: optionStringValue(options, "started_at", "started-at"),
|
|
EndedAt: optionStringValue(options, "ended_at", "ended-at"),
|
|
})
|
|
if err != nil {
|
|
return core.Result{Value: err, OK: false}
|
|
}
|
|
return core.Result{Value: output, OK: true}
|
|
}
|
|
|
|
// result := c.Action("sprint.get").Run(ctx, core.NewOptions(core.Option{Key: "slug", Value: "ax-follow-up"}))
|
|
func (s *PrepSubsystem) handleSprintGet(ctx context.Context, options core.Options) core.Result {
|
|
_, output, err := s.sprintGet(ctx, nil, SprintGetInput{
|
|
ID: optionStringValue(options, "id", "_arg"),
|
|
Slug: optionStringValue(options, "slug"),
|
|
})
|
|
if err != nil {
|
|
return core.Result{Value: err, OK: false}
|
|
}
|
|
return core.Result{Value: output, OK: true}
|
|
}
|
|
|
|
// result := c.Action("sprint.list").Run(ctx, core.NewOptions(core.Option{Key: "status", Value: "active"}))
|
|
func (s *PrepSubsystem) handleSprintList(ctx context.Context, options core.Options) core.Result {
|
|
_, output, err := s.sprintList(ctx, nil, SprintListInput{
|
|
Status: optionStringValue(options, "status"),
|
|
Limit: optionIntValue(options, "limit"),
|
|
})
|
|
if err != nil {
|
|
return core.Result{Value: err, OK: false}
|
|
}
|
|
return core.Result{Value: output, OK: true}
|
|
}
|
|
|
|
// result := c.Action("sprint.update").Run(ctx, core.NewOptions(core.Option{Key: "slug", Value: "ax-follow-up"}))
|
|
func (s *PrepSubsystem) handleSprintUpdate(ctx context.Context, options core.Options) core.Result {
|
|
_, output, err := s.sprintUpdate(ctx, nil, SprintUpdateInput{
|
|
ID: optionStringValue(options, "id", "_arg"),
|
|
Slug: optionStringValue(options, "slug"),
|
|
Title: optionStringValue(options, "title"),
|
|
Goal: optionStringValue(options, "goal"),
|
|
Status: optionStringValue(options, "status"),
|
|
Metadata: optionAnyMapValue(options, "metadata"),
|
|
StartedAt: optionStringValue(options, "started_at", "started-at"),
|
|
EndedAt: optionStringValue(options, "ended_at", "ended-at"),
|
|
})
|
|
if err != nil {
|
|
return core.Result{Value: err, OK: false}
|
|
}
|
|
return core.Result{Value: output, OK: true}
|
|
}
|
|
|
|
// result := c.Action("sprint.archive").Run(ctx, core.NewOptions(core.Option{Key: "slug", Value: "ax-follow-up"}))
|
|
func (s *PrepSubsystem) handleSprintArchive(ctx context.Context, options core.Options) core.Result {
|
|
_, output, err := s.sprintArchive(ctx, nil, SprintArchiveInput{
|
|
ID: optionStringValue(options, "id", "_arg"),
|
|
Slug: optionStringValue(options, "slug"),
|
|
})
|
|
if err != nil {
|
|
return core.Result{Value: err, OK: false}
|
|
}
|
|
return core.Result{Value: output, OK: true}
|
|
}
|
|
|
|
func (s *PrepSubsystem) registerSprintTools(server *mcp.Server) {
|
|
mcp.AddTool(server, &mcp.Tool{
|
|
Name: "sprint_create",
|
|
Description: "Create a tracked platform sprint with goal, schedule, and metadata.",
|
|
}, s.sprintCreate)
|
|
mcp.AddTool(server, &mcp.Tool{
|
|
Name: "agentic_sprint_create",
|
|
Description: "Create a tracked platform sprint with goal, schedule, and metadata.",
|
|
}, s.sprintCreate)
|
|
|
|
mcp.AddTool(server, &mcp.Tool{
|
|
Name: "sprint_get",
|
|
Description: "Read a tracked platform sprint by slug.",
|
|
}, s.sprintGet)
|
|
mcp.AddTool(server, &mcp.Tool{
|
|
Name: "agentic_sprint_get",
|
|
Description: "Read a tracked platform sprint by slug.",
|
|
}, s.sprintGet)
|
|
|
|
mcp.AddTool(server, &mcp.Tool{
|
|
Name: "sprint_list",
|
|
Description: "List tracked platform sprints with optional status and limit filters.",
|
|
}, s.sprintList)
|
|
mcp.AddTool(server, &mcp.Tool{
|
|
Name: "agentic_sprint_list",
|
|
Description: "List tracked platform sprints with optional status and limit filters.",
|
|
}, s.sprintList)
|
|
|
|
mcp.AddTool(server, &mcp.Tool{
|
|
Name: "sprint_update",
|
|
Description: "Update fields on a tracked platform sprint by slug.",
|
|
}, s.sprintUpdate)
|
|
mcp.AddTool(server, &mcp.Tool{
|
|
Name: "agentic_sprint_update",
|
|
Description: "Update fields on a tracked platform sprint by slug.",
|
|
}, s.sprintUpdate)
|
|
|
|
mcp.AddTool(server, &mcp.Tool{
|
|
Name: "sprint_archive",
|
|
Description: "Archive a tracked platform sprint by slug.",
|
|
}, s.sprintArchive)
|
|
mcp.AddTool(server, &mcp.Tool{
|
|
Name: "agentic_sprint_archive",
|
|
Description: "Archive a tracked platform sprint by slug.",
|
|
}, s.sprintArchive)
|
|
}
|
|
|
|
func (s *PrepSubsystem) sprintCreate(ctx context.Context, _ *mcp.CallToolRequest, input SprintCreateInput) (*mcp.CallToolResult, SprintOutput, error) {
|
|
if input.Title == "" {
|
|
return nil, SprintOutput{}, core.E("sprintCreate", "title is required", nil)
|
|
}
|
|
|
|
body := map[string]any{
|
|
"title": input.Title,
|
|
}
|
|
if input.Goal != "" {
|
|
body["goal"] = input.Goal
|
|
}
|
|
if input.Status != "" {
|
|
body["status"] = input.Status
|
|
}
|
|
if len(input.Metadata) > 0 {
|
|
body["metadata"] = input.Metadata
|
|
}
|
|
if input.StartedAt != "" {
|
|
body["started_at"] = input.StartedAt
|
|
}
|
|
if input.EndedAt != "" {
|
|
body["ended_at"] = input.EndedAt
|
|
}
|
|
|
|
result := s.platformPayload(ctx, "sprint.create", "POST", "/v1/sprints", body)
|
|
if !result.OK {
|
|
return nil, SprintOutput{}, resultErrorValue("sprint.create", result)
|
|
}
|
|
|
|
return nil, SprintOutput{
|
|
Success: true,
|
|
Sprint: parseSprint(payloadResourceMap(result.Value.(map[string]any), "sprint")),
|
|
}, nil
|
|
}
|
|
|
|
func (s *PrepSubsystem) sprintGet(ctx context.Context, _ *mcp.CallToolRequest, input SprintGetInput) (*mcp.CallToolResult, SprintOutput, error) {
|
|
identifier := sprintIdentifier(input.Slug, input.ID)
|
|
if identifier == "" {
|
|
return nil, SprintOutput{}, core.E("sprintGet", "id or slug is required", nil)
|
|
}
|
|
|
|
result := s.platformPayload(ctx, "sprint.get", "GET", core.Concat("/v1/sprints/", identifier), nil)
|
|
if !result.OK {
|
|
return nil, SprintOutput{}, resultErrorValue("sprint.get", result)
|
|
}
|
|
|
|
return nil, SprintOutput{
|
|
Success: true,
|
|
Sprint: parseSprint(payloadResourceMap(result.Value.(map[string]any), "sprint")),
|
|
}, nil
|
|
}
|
|
|
|
func (s *PrepSubsystem) sprintList(ctx context.Context, _ *mcp.CallToolRequest, input SprintListInput) (*mcp.CallToolResult, SprintListOutput, error) {
|
|
path := "/v1/sprints"
|
|
path = appendQueryParam(path, "status", input.Status)
|
|
if input.Limit > 0 {
|
|
path = appendQueryParam(path, "limit", core.Sprint(input.Limit))
|
|
}
|
|
|
|
result := s.platformPayload(ctx, "sprint.list", "GET", path, nil)
|
|
if !result.OK {
|
|
return nil, SprintListOutput{}, resultErrorValue("sprint.list", result)
|
|
}
|
|
|
|
return nil, parseSprintListOutput(result.Value.(map[string]any)), nil
|
|
}
|
|
|
|
func (s *PrepSubsystem) sprintUpdate(ctx context.Context, _ *mcp.CallToolRequest, input SprintUpdateInput) (*mcp.CallToolResult, SprintOutput, error) {
|
|
identifier := sprintIdentifier(input.Slug, input.ID)
|
|
if identifier == "" {
|
|
return nil, SprintOutput{}, core.E("sprintUpdate", "id or slug is required", nil)
|
|
}
|
|
|
|
body := map[string]any{}
|
|
if input.Title != "" {
|
|
body["title"] = input.Title
|
|
}
|
|
if input.Goal != "" {
|
|
body["goal"] = input.Goal
|
|
}
|
|
if input.Status != "" {
|
|
body["status"] = input.Status
|
|
}
|
|
if len(input.Metadata) > 0 {
|
|
body["metadata"] = input.Metadata
|
|
}
|
|
if input.StartedAt != "" {
|
|
body["started_at"] = input.StartedAt
|
|
}
|
|
if input.EndedAt != "" {
|
|
body["ended_at"] = input.EndedAt
|
|
}
|
|
if len(body) == 0 {
|
|
return nil, SprintOutput{}, core.E("sprintUpdate", "at least one field is required", nil)
|
|
}
|
|
|
|
result := s.platformPayload(ctx, "sprint.update", "PATCH", core.Concat("/v1/sprints/", identifier), body)
|
|
if !result.OK {
|
|
return nil, SprintOutput{}, resultErrorValue("sprint.update", result)
|
|
}
|
|
|
|
return nil, SprintOutput{
|
|
Success: true,
|
|
Sprint: parseSprint(payloadResourceMap(result.Value.(map[string]any), "sprint")),
|
|
}, nil
|
|
}
|
|
|
|
func (s *PrepSubsystem) sprintArchive(ctx context.Context, _ *mcp.CallToolRequest, input SprintArchiveInput) (*mcp.CallToolResult, SprintArchiveOutput, error) {
|
|
identifier := sprintIdentifier(input.Slug, input.ID)
|
|
if identifier == "" {
|
|
return nil, SprintArchiveOutput{}, core.E("sprintArchive", "id or slug is required", nil)
|
|
}
|
|
|
|
result := s.platformPayload(ctx, "sprint.archive", "DELETE", core.Concat("/v1/sprints/", identifier), nil)
|
|
if !result.OK {
|
|
return nil, SprintArchiveOutput{}, resultErrorValue("sprint.archive", result)
|
|
}
|
|
|
|
output := SprintArchiveOutput{
|
|
Success: true,
|
|
Archived: identifier,
|
|
}
|
|
if values := payloadResourceMap(result.Value.(map[string]any), "sprint", "result"); len(values) > 0 {
|
|
if slug := stringValue(values["slug"]); slug != "" {
|
|
output.Archived = slug
|
|
}
|
|
if value, ok := boolValueOK(values["success"]); ok {
|
|
output.Success = value
|
|
}
|
|
}
|
|
return nil, output, nil
|
|
}
|
|
|
|
func sprintIdentifier(values ...string) string {
|
|
for _, value := range values {
|
|
if trimmed := core.Trim(value); trimmed != "" {
|
|
return trimmed
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func parseSprint(values map[string]any) Sprint {
|
|
return Sprint{
|
|
ID: intValue(values["id"]),
|
|
WorkspaceID: intValue(values["workspace_id"]),
|
|
Slug: stringValue(values["slug"]),
|
|
Title: stringValue(values["title"]),
|
|
Goal: stringValue(values["goal"]),
|
|
Status: stringValue(values["status"]),
|
|
Metadata: anyMapValue(values["metadata"]),
|
|
StartedAt: stringValue(values["started_at"]),
|
|
EndedAt: stringValue(values["ended_at"]),
|
|
CreatedAt: stringValue(values["created_at"]),
|
|
UpdatedAt: stringValue(values["updated_at"]),
|
|
}
|
|
}
|
|
|
|
func parseSprintListOutput(payload map[string]any) SprintListOutput {
|
|
sprintsData := payloadDataSlice(payload, "sprints")
|
|
sprints := make([]Sprint, 0, len(sprintsData))
|
|
for _, values := range sprintsData {
|
|
sprints = append(sprints, parseSprint(values))
|
|
}
|
|
|
|
count := mapIntValue(payload, "total", "count")
|
|
if count == 0 {
|
|
count = mapIntValue(payloadDataMap(payload), "total", "count")
|
|
}
|
|
if count == 0 {
|
|
count = len(sprints)
|
|
}
|
|
|
|
return SprintListOutput{
|
|
Success: true,
|
|
Count: count,
|
|
Sprints: sprints,
|
|
}
|
|
}
|