Files
amcs/internal/tools/helpers.go
Hein f41c512f36 test(tools): add unit tests for error handling functions
* Implement tests for error functions like errRequiredField, errInvalidField, and errEntityNotFound.
* Ensure proper metadata is returned for various error scenarios.
* Validate error handling in CRM, Files, and other tools.
* Introduce tests for parsing stored file IDs and UUIDs.
* Enhance coverage for helper functions related to project resolution and session management.
2026-03-31 15:10:07 +02:00

123 lines
3.2 KiB
Go

package tools
import (
"context"
"fmt"
"strings"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/modelcontextprotocol/go-sdk/mcp"
"git.warky.dev/wdevs/amcs/internal/mcperrors"
"git.warky.dev/wdevs/amcs/internal/session"
"git.warky.dev/wdevs/amcs/internal/store"
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
)
func parseUUID(id string) (uuid.UUID, error) {
trimmed := strings.TrimSpace(id)
parsed, err := uuid.Parse(trimmed)
if err != nil {
return uuid.Nil, newMCPError(
codeInvalidID,
fmt.Sprintf("invalid id %q", id),
mcpErrorData{
Type: mcperrors.TypeInvalidID,
Field: "id",
Value: trimmed,
Detail: err.Error(),
Hint: "pass a valid UUID",
},
)
}
return parsed, nil
}
func sessionID(req *mcp.CallToolRequest) (string, error) {
if req == nil || req.Session == nil || req.Session.ID() == "" {
return "", newMCPError(
codeSessionRequired,
"tool requires an MCP session; use a stateful MCP client for session-scoped operations",
mcpErrorData{
Type: mcperrors.TypeSessionRequired,
Hint: "use a stateful MCP client for session-scoped operations",
},
)
}
return req.Session.ID(), nil
}
func resolveProject(ctx context.Context, db *store.DB, sessions *session.ActiveProjects, req *mcp.CallToolRequest, raw string, required bool) (*thoughttypes.Project, error) {
projectRef := strings.TrimSpace(raw)
if projectRef == "" && sessions != nil && req != nil && req.Session != nil {
if activeID, ok := sessions.Get(req.Session.ID()); ok {
project, err := db.GetProject(ctx, activeID.String())
if err == nil {
return &project, nil
}
if err != pgx.ErrNoRows {
return nil, err
}
}
}
if projectRef == "" {
if required {
return nil, newMCPError(
codeProjectRequired,
"project is required; pass project explicitly or call set_active_project in this MCP session first",
mcpErrorData{
Type: mcperrors.TypeProjectRequired,
Field: "project",
Hint: "pass project explicitly or call set_active_project in this MCP session first",
},
)
}
return nil, nil
}
project, err := db.GetProject(ctx, projectRef)
if err != nil {
if err == pgx.ErrNoRows {
return nil, newMCPError(
codeProjectNotFound,
fmt.Sprintf("project %q not found", projectRef),
mcpErrorData{
Type: mcperrors.TypeProjectNotFound,
Field: "project",
Project: projectRef,
},
)
}
return nil, err
}
return &project, nil
}
func formatContextBlock(header string, lines []string) string {
if len(lines) == 0 {
return header + "\n\nNo matching thoughts."
}
return header + "\n\n" + strings.Join(lines, "\n\n")
}
func thoughtContextLine(index int, content string, metadata thoughttypes.ThoughtMetadata, similarity float64) string {
label := fmt.Sprintf("%d. %s", index+1, strings.TrimSpace(content))
parts := make([]string, 0, 3)
if len(metadata.Topics) > 0 {
parts = append(parts, "topics="+strings.Join(metadata.Topics, ", "))
}
if metadata.Type != "" {
parts = append(parts, "type="+metadata.Type)
}
if similarity > 0 {
parts = append(parts, fmt.Sprintf("similarity=%.3f", similarity))
}
if len(parts) == 0 {
return label
}
return label + "\n[" + strings.Join(parts, " | ") + "]"
}