package tools import ( "context" "encoding/json" "fmt" "log/slog" "github.com/mark3labs/mcp-go/mcp" "spark-mcp-go/internal/logging" ) // textResult wraps a string into a text CallToolResult. func textResult(s string) *mcp.CallToolResult { return mcp.NewToolResultText(s) } // errResult wraps a string into an error CallToolResult. // MCP protocol errors are reserved for exceptional conditions; business // errors are reported inside the tool result with IsError set by the library. func errResult(s string) *mcp.CallToolResult { return mcp.NewToolResultError(s) } // encodeJSON marshals v to JSON. On failure it returns a string literal that // embeds the error so the caller never receives a nil or empty result. func encodeJSON(v any) string { b, err := json.Marshal(v) if err != nil { return fmt.Sprintf("", err) } return string(b) } // startToolCall starts per-tool-call logging. If logging is not initialized // (e.g. during tests) it falls back to a no-op logger so handlers never panic. func startToolCall(ctx context.Context, logger *slog.Logger, toolName string, params any) logging.ToolCallLogger { callLog, err := logging.StartToolCall(ctx, logger, toolName, params) if err != nil { return &noopToolCallLogger{} } return callLog } type noopToolCallLogger struct{} func (n *noopToolCallLogger) WithResult(any) {} func (n *noopToolCallLogger) WithError(error) {} func (n *noopToolCallLogger) End() {} // truncateMiddle limits s to roughly max bytes by keeping the first and // last halves, separated by a marker. It preserves UTF-8 rune boundaries // so the result never starts or ends with a broken multi-byte rune. func truncateMiddle(s string, max int) string { if len(s) <= max { return s } half := max / 2 start := s[:half] end := s[len(s)-half:] for len(start) > 0 && start[len(start)-1] >= 0x80 && start[len(start)-1] < 0xC0 { start = start[:len(start)-1] } for len(end) > 0 && end[0]&0xC0 == 0x80 { end = end[1:] } return start + "\n... [truncated middle] ...\n" + end } // truncateTail keeps the last max bytes of s, preserving UTF-8 rune boundaries. func truncateTail(s string, max int) string { if len(s) <= max { return s } start := len(s) - max for start < len(s) && s[start]&0xC0 == 0x80 { start++ } return s[start:] }