* 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.
123 lines
3.2 KiB
Go
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, " | ") + "]"
|
|
}
|