1#!/usr/bin/env python3

2"""Message formatting utilities for MCP tools.

3

4 This module provides consistent message formatting across all tool implementations,

5 eliminating duplication and ensuring uniform user experience.

6"""

7

8 from __future__ import annotations

9

10 from datetime import datetime

11 from typing import Any

12

13

14 class ToolMessages:

15 """Centralized tool message formatting.

16

17 This class provides static methods for formatting common tool messages

18 with consistent styling and structure.

19 """

20

21 @staticmethod

22 def not_available(feature: str, install_hint: str = "") -> str:

23 """Format feature unavailable message.

24

25 Args:

26 feature: Name of the unavailable feature

27 install_hint: Optional installation instructions

28

29 Returns:

30 Formatted error message

31

32 Example:

33 >>> ToolMessages.not_available("Database", "uv sync --extra embeddings")

34 '❌ Database not available. Install: uv sync --extra embeddings'

35

36 """

37 msg = f"❌ {feature} not available"

38 if install_hint: 38 ↛ 43 line 38 didn't jump to line 43 because the condition on line 38 was always true

39 if not install_hint.startswith("Install"): 39 ↛ 42 line 39 didn't jump to line 42 because the condition on line 39 was always true

40 msg += f". Install: {install_hint}"

41 else:

42 msg += f". {install_hint}"

43 return msg

44

45 @staticmethod

46 def operation_failed(operation: str, error: Exception | str) -> str:

47 """Format operation failure message.

48

49 Args:

50 operation: Name of the failed operation

51 error: Exception or error message

52

53 Returns:

54 Formatted error message

55

56 Example:

57 >>> ToolMessages.operation_failed("Search", ValueError("Bad input"))

58 '❌ Search failed: Bad input'

59

60 """

61 error_str = str(error)

62 # Remove "Exception: " prefix if present

63 if ": " in error_str and error_str.split(": ", maxsplit=1)[0].endswith("Error"): 63 ↛ 64 line 63 didn't jump to line 64 because the condition on line 63 was never true

64 error_str = ": ".join(error_str.split(": ")[1:])

65 return f"❌ {operation} failed: {error_str}"

66

67 @staticmethod

68 def success(message: str, details: dict[str, Any] | None = None) -> str:

69 """Format success message with optional details.

70

71 Args:

72 message: Main success message

73 details: Optional dictionary of details to display

74

75 Returns:

76 Formatted success message with details

77

78 Example:

79 >>> ToolMessages.success("Stored", {"items": 5, "time": "1.2s"})

80 '✅ Stored

81 • items: 5

82 • time: 1.2s'

83

84 """

85 lines = [f"✅ {message}"]

86 if details:

87 for key, value in details.items():

88 lines.append(f" • {key}: {value}")

89 return "\n".join(lines)

90

91 @staticmethod

92 def validation_error(field: str, message: str) -> str:

93 """Format input validation error.

94

95 Args:

96 field: Name of the invalid field

97 message: Validation error description

98

99 Returns:

100 Formatted validation error

101

102 Example:

103 >>> ToolMessages.validation_error("email", "Invalid format")

104 '❌ Validation error: email - Invalid format'

105

106 """

107 return f"❌ Validation error: {field} - {message}"

108

109 @staticmethod

110 def empty_results(operation: str, suggestion: str = "") -> str:

111 """Format message for empty results.

112

113 Args:

114 operation: Operation that returned no results

115 suggestion: Optional suggestion for the user

116

117 Returns:

118 Formatted empty results message

119

120 Example:

121 >>> ToolMessages.empty_results("Search", "Try broader terms")

122 'i️ No results found for Search. Try broader terms'

123

124 """

125 msg = f"ℹ️ No results found for {operation}"

126 if suggestion:

127 msg += f". {suggestion}"

128 return msg

129

130 @staticmethod

131 def format_list_item(emoji: str, label: str, value: Any) -> str:

132 """Format a single list item with emoji and label.

133

134 Args:

135 emoji: Emoji to use for the item

136 label: Label for the item

137 value: Value to display

138

139 Returns:

140 Formatted list item

141

142 Example:

143 >>> ToolMessages.format_list_item("📝", "Content", "Hello world")

144 '📝 Content: Hello world'

145

146 """

147 return f"{emoji} {label}: {value}"

148

149 @staticmethod

150 def format_timestamp(dt: datetime | None = None) -> str:

151 """Format a timestamp consistently.

152

153 Args:

154 dt: Datetime to format (defaults to now)

155

156 Returns:

157 Formatted timestamp string

158

159 Example:

160 >>> ToolMessages.format_timestamp()

161 '2025-01-12 14:30:45'

162

163 """

164 if dt is None:

165 dt = datetime.now()

166 return dt.strftime("%Y-%m-%d %H:%M:%S")

167

168 @staticmethod

169 def format_count(count: int, singular: str, plural: str | None = None) -> str:

170 """Format a count with appropriate singular/plural form.

171

172 Args:

173 count: Number to format

174 singular: Singular form of the noun

175 plural: Plural form (defaults to singular + 's')

176

177 Returns:

178 Formatted count string

179

180 Example:

181 >>> ToolMessages.format_count(1, "result")

182 '1 result'

183 >>> ToolMessages.format_count(5, "match", "matches")

184 '5 matches'

185

186 """

187 if plural is None:

188 plural = f"{singular}s"

189 word = singular if count == 1 else plural

190 return f"{count} {word}"

191

192 @staticmethod

193 def format_progress(current: int, total: int, operation: str = "") -> str:

194 """Format a progress indicator.

195

196 Args:

197 current: Current progress

198 total: Total items

199 operation: Optional operation description

200

201 Returns:

202 Formatted progress string

203

204 Example:

205 >>> ToolMessages.format_progress(5, 10, "Processing")

206 'Processing: 5/10 (50%)'

207

208 """

209 percentage = int((current / total) * 100) if total > 0 else 0

210 base = f"{current}/{total} ({percentage}%)"

211 if operation:

212 return f"{operation}: {base}"

213 return base

214

215 @staticmethod

216 def format_duration(seconds: float) -> str:

217 """Format a duration in seconds to human-readable form.

218

219 Args:

220 seconds: Duration in seconds

221

222 Returns:

223 Formatted duration string

224

225 Example:

226 >>> ToolMessages.format_duration(65.5)

227 '1m 5.5s'

228 >>> ToolMessages.format_duration(3.2)

229 '3.2s'

230

231 """

232 if seconds < 60:

233 return f"{seconds:.1f}s"

234 minutes = int(seconds // 60)

235 remaining_seconds = seconds % 60

236 return f"{minutes}m {remaining_seconds:.1f}s"

237

238 @staticmethod

239 def format_bytes(bytes_count: int) -> str:

240 """Format byte count to human-readable form.

241

242 Args:

243 bytes_count: Number of bytes

244

245 Returns:

246 Formatted byte string with appropriate unit

247

248 Example:

249 >>> ToolMessages.format_bytes(1500)

250 '1.5 KB'

251 >>> ToolMessages.format_bytes(1_500_000)

252 '1.4 MB'

253

254 """

255 bytes_float = float(bytes_count)

256 for unit in ("B", "KB", "MB", "GB"):

257 if bytes_float < 1024.0:

258 return f"{bytes_float:.1f} {unit}"

259 bytes_float /= 1024.0

260 return f"{bytes_float:.1f} TB"

261

262 @staticmethod

263 def format_result_summary(

264 results: list[Any],

265 operation: str,

266 show_count: bool = True,

267 max_display: int = 5,

268 ) -> str:

269 """Format a summary of results from an operation.

270

271 Args:

272 results: List of results

273 operation: Operation that produced results

274 show_count: Whether to show result count

275 max_display: Maximum number of results to show details for

276

277 Returns:

278 Formatted result summary

279

280 Example:

281 >>> results = ["a", "b", "c"]

282 >>> ToolMessages.format_result_summary(results, "Search")

283 '✅ Search complete: 3 results'

284

285 """

286 count = len(results)

287

288 if count == 0:

289 return ToolMessages.empty_results(operation)

290

291 lines = []

292 if show_count:

293 count_str = ToolMessages.format_count(count, "result")

294 lines.append(f"✅ {operation} complete: {count_str}")

295 else:

296 lines.append(f"✅ {operation} complete")

297

298 # Show summary of first few results if they're simple types

299 if max_display > 0 and results:

300 sample = results[:max_display]

301 for i, result in enumerate(sample, 1):

302 if isinstance(result, (str, int, float, bool)):

303 lines.append(f" {i}. {result}")

304

305 if count > max_display:

306 lines.append(f" ... and {count - max_display} more")

307

308 return "\n".join(lines)

309

310 @staticmethod

311 def truncate_text(text: str, max_length: int = 100, suffix: str = "...") -> str:

312 """Truncate text to maximum length with suffix.

313

314 Args:

315 text: Text to truncate

316 max_length: Maximum length before truncation

317 suffix: Suffix to add when truncated

318

319 Returns:

320 Truncated text with suffix if needed

321

322 Example:

323 >>> ToolMessages.truncate_text("Hello world this is long", 15)

324 'Hello world ...'

325

326 """

327 if len(text) <= max_length: 327 ↛ 329 line 327 didn't jump to line 329 because the condition on line 327 was always true

328 return text

329 return text[: max_length - len(suffix)] + suffix