fastmcp 2.12.1__py3-none-any.whl → 2.13.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. fastmcp/__init__.py +2 -2
  2. fastmcp/cli/cli.py +56 -36
  3. fastmcp/cli/install/__init__.py +2 -0
  4. fastmcp/cli/install/claude_code.py +7 -16
  5. fastmcp/cli/install/claude_desktop.py +4 -12
  6. fastmcp/cli/install/cursor.py +20 -30
  7. fastmcp/cli/install/gemini_cli.py +241 -0
  8. fastmcp/cli/install/mcp_json.py +4 -12
  9. fastmcp/cli/run.py +15 -94
  10. fastmcp/client/__init__.py +9 -9
  11. fastmcp/client/auth/oauth.py +117 -206
  12. fastmcp/client/client.py +123 -47
  13. fastmcp/client/elicitation.py +6 -1
  14. fastmcp/client/logging.py +18 -14
  15. fastmcp/client/oauth_callback.py +85 -171
  16. fastmcp/client/sampling.py +1 -1
  17. fastmcp/client/transports.py +81 -26
  18. fastmcp/contrib/component_manager/__init__.py +1 -1
  19. fastmcp/contrib/component_manager/component_manager.py +2 -2
  20. fastmcp/contrib/component_manager/component_service.py +7 -7
  21. fastmcp/contrib/mcp_mixin/README.md +35 -4
  22. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  23. fastmcp/contrib/mcp_mixin/mcp_mixin.py +54 -7
  24. fastmcp/experimental/sampling/handlers/openai.py +2 -2
  25. fastmcp/experimental/server/openapi/__init__.py +5 -8
  26. fastmcp/experimental/server/openapi/components.py +11 -7
  27. fastmcp/experimental/server/openapi/routing.py +2 -2
  28. fastmcp/experimental/utilities/openapi/__init__.py +10 -15
  29. fastmcp/experimental/utilities/openapi/director.py +16 -10
  30. fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
  31. fastmcp/experimental/utilities/openapi/models.py +3 -3
  32. fastmcp/experimental/utilities/openapi/parser.py +37 -16
  33. fastmcp/experimental/utilities/openapi/schemas.py +33 -7
  34. fastmcp/mcp_config.py +3 -4
  35. fastmcp/prompts/__init__.py +1 -1
  36. fastmcp/prompts/prompt.py +32 -27
  37. fastmcp/prompts/prompt_manager.py +16 -101
  38. fastmcp/resources/__init__.py +5 -5
  39. fastmcp/resources/resource.py +28 -20
  40. fastmcp/resources/resource_manager.py +9 -168
  41. fastmcp/resources/template.py +119 -27
  42. fastmcp/resources/types.py +30 -24
  43. fastmcp/server/__init__.py +1 -1
  44. fastmcp/server/auth/__init__.py +9 -5
  45. fastmcp/server/auth/auth.py +80 -47
  46. fastmcp/server/auth/handlers/authorize.py +326 -0
  47. fastmcp/server/auth/jwt_issuer.py +236 -0
  48. fastmcp/server/auth/middleware.py +96 -0
  49. fastmcp/server/auth/oauth_proxy.py +1556 -265
  50. fastmcp/server/auth/oidc_proxy.py +412 -0
  51. fastmcp/server/auth/providers/auth0.py +193 -0
  52. fastmcp/server/auth/providers/aws.py +263 -0
  53. fastmcp/server/auth/providers/azure.py +314 -129
  54. fastmcp/server/auth/providers/bearer.py +1 -1
  55. fastmcp/server/auth/providers/debug.py +114 -0
  56. fastmcp/server/auth/providers/descope.py +229 -0
  57. fastmcp/server/auth/providers/discord.py +308 -0
  58. fastmcp/server/auth/providers/github.py +31 -6
  59. fastmcp/server/auth/providers/google.py +50 -7
  60. fastmcp/server/auth/providers/in_memory.py +27 -3
  61. fastmcp/server/auth/providers/introspection.py +281 -0
  62. fastmcp/server/auth/providers/jwt.py +48 -31
  63. fastmcp/server/auth/providers/oci.py +233 -0
  64. fastmcp/server/auth/providers/scalekit.py +238 -0
  65. fastmcp/server/auth/providers/supabase.py +188 -0
  66. fastmcp/server/auth/providers/workos.py +37 -15
  67. fastmcp/server/context.py +194 -67
  68. fastmcp/server/dependencies.py +56 -16
  69. fastmcp/server/elicitation.py +1 -1
  70. fastmcp/server/http.py +57 -18
  71. fastmcp/server/low_level.py +121 -2
  72. fastmcp/server/middleware/__init__.py +1 -1
  73. fastmcp/server/middleware/caching.py +476 -0
  74. fastmcp/server/middleware/error_handling.py +14 -10
  75. fastmcp/server/middleware/logging.py +158 -116
  76. fastmcp/server/middleware/middleware.py +30 -16
  77. fastmcp/server/middleware/rate_limiting.py +3 -3
  78. fastmcp/server/middleware/tool_injection.py +116 -0
  79. fastmcp/server/openapi.py +15 -7
  80. fastmcp/server/proxy.py +22 -11
  81. fastmcp/server/server.py +744 -254
  82. fastmcp/settings.py +65 -15
  83. fastmcp/tools/__init__.py +1 -1
  84. fastmcp/tools/tool.py +173 -108
  85. fastmcp/tools/tool_manager.py +30 -112
  86. fastmcp/tools/tool_transform.py +13 -11
  87. fastmcp/utilities/cli.py +67 -28
  88. fastmcp/utilities/components.py +7 -2
  89. fastmcp/utilities/inspect.py +79 -23
  90. fastmcp/utilities/json_schema.py +21 -4
  91. fastmcp/utilities/json_schema_type.py +4 -4
  92. fastmcp/utilities/logging.py +182 -10
  93. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  94. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  95. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +10 -45
  96. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +8 -7
  97. fastmcp/utilities/mcp_server_config/v1/schema.json +5 -1
  98. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  99. fastmcp/utilities/openapi.py +11 -11
  100. fastmcp/utilities/tests.py +93 -10
  101. fastmcp/utilities/types.py +87 -21
  102. fastmcp/utilities/ui.py +626 -0
  103. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/METADATA +141 -60
  104. fastmcp-2.13.2.dist-info/RECORD +144 -0
  105. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
  106. fastmcp/cli/claude.py +0 -144
  107. fastmcp-2.12.1.dist-info/RECORD +0 -128
  108. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
  109. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,626 @@
1
+ """
2
+ Shared UI utilities for FastMCP HTML pages.
3
+
4
+ This module provides reusable HTML/CSS components for OAuth callbacks,
5
+ consent pages, and other user-facing interfaces.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import html
11
+
12
+ from starlette.responses import HTMLResponse
13
+
14
+ # FastMCP branding
15
+ FASTMCP_LOGO_URL = "https://gofastmcp.com/assets/brand/blue-logo.png"
16
+
17
+ # Base CSS styles shared across all FastMCP pages
18
+ BASE_STYLES = """
19
+ * {
20
+ margin: 0;
21
+ padding: 0;
22
+ box-sizing: border-box;
23
+ }
24
+
25
+ body {
26
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
27
+ margin: 0;
28
+ padding: 0;
29
+ min-height: 100vh;
30
+ display: flex;
31
+ align-items: center;
32
+ justify-content: center;
33
+ background: #f9fafb;
34
+ color: #0a0a0a;
35
+ }
36
+
37
+ .container {
38
+ background: #ffffff;
39
+ border: 1px solid #e5e7eb;
40
+ padding: 3rem 2.5rem;
41
+ border-radius: 1rem;
42
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
43
+ text-align: center;
44
+ max-width: 36rem;
45
+ margin: 1rem;
46
+ width: 100%;
47
+ }
48
+
49
+ @media (max-width: 640px) {
50
+ .container {
51
+ padding: 2rem 1.5rem;
52
+ margin: 0.5rem;
53
+ }
54
+ }
55
+
56
+ .logo {
57
+ width: 64px;
58
+ height: auto;
59
+ margin-bottom: 1.5rem;
60
+ display: block;
61
+ margin-left: auto;
62
+ margin-right: auto;
63
+ }
64
+
65
+ h1 {
66
+ font-size: 1.5rem;
67
+ font-weight: 600;
68
+ margin-bottom: 1.5rem;
69
+ color: #111827;
70
+ }
71
+ """
72
+
73
+ # Button styles
74
+ BUTTON_STYLES = """
75
+ .button-group {
76
+ display: flex;
77
+ gap: 0.75rem;
78
+ margin-top: 1.5rem;
79
+ justify-content: center;
80
+ }
81
+
82
+ button {
83
+ padding: 0.75rem 2rem;
84
+ font-size: 0.9375rem;
85
+ font-weight: 500;
86
+ border-radius: 0.5rem;
87
+ border: none;
88
+ cursor: pointer;
89
+ transition: all 0.15s;
90
+ font-family: inherit;
91
+ }
92
+
93
+ button:hover {
94
+ transform: translateY(-1px);
95
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
96
+ }
97
+
98
+ .btn-approve, .btn-primary {
99
+ background: #10b981;
100
+ color: #ffffff;
101
+ min-width: 120px;
102
+ }
103
+
104
+ .btn-deny, .btn-secondary {
105
+ background: #6b7280;
106
+ color: #ffffff;
107
+ min-width: 120px;
108
+ }
109
+ """
110
+
111
+ # Info box / message box styles
112
+ INFO_BOX_STYLES = """
113
+ .info-box {
114
+ background: #f0f9ff;
115
+ border: 1px solid #bae6fd;
116
+ border-radius: 0.5rem;
117
+ padding: 1rem;
118
+ margin-bottom: 1.5rem;
119
+ text-align: left;
120
+ font-size: 0.9375rem;
121
+ line-height: 1.5;
122
+ color: #374151;
123
+ }
124
+
125
+ .info-box p {
126
+ margin-bottom: 0.5rem;
127
+ }
128
+
129
+ .info-box p:last-child {
130
+ margin-bottom: 0;
131
+ }
132
+
133
+ .info-box.centered {
134
+ text-align: center;
135
+ }
136
+
137
+ .info-box.error {
138
+ background: #fef2f2;
139
+ border-color: #fecaca;
140
+ color: #991b1b;
141
+ }
142
+
143
+ .info-box strong {
144
+ color: #0ea5e9;
145
+ font-weight: 600;
146
+ }
147
+
148
+ .info-box .server-name-link {
149
+ color: #0ea5e9;
150
+ text-decoration: underline;
151
+ font-weight: 600;
152
+ cursor: pointer;
153
+ transition: opacity 0.15s;
154
+ }
155
+
156
+ .info-box .server-name-link:hover {
157
+ opacity: 0.8;
158
+ }
159
+
160
+ /* Monospace info box - gray styling with code font */
161
+ .info-box-mono {
162
+ background: #f9fafb;
163
+ border: 1px solid #e5e7eb;
164
+ border-radius: 0.5rem;
165
+ padding: 0.875rem;
166
+ margin: 1.25rem 0;
167
+ font-size: 0.875rem;
168
+ color: #6b7280;
169
+ font-family: 'SF Mono', 'Monaco', 'Consolas', 'Courier New', monospace;
170
+ text-align: left;
171
+ }
172
+
173
+ .info-box-mono.centered {
174
+ text-align: center;
175
+ }
176
+
177
+ .info-box-mono.error {
178
+ background: #fef2f2;
179
+ border-color: #fecaca;
180
+ color: #991b1b;
181
+ }
182
+
183
+ .info-box-mono strong {
184
+ color: #111827;
185
+ font-weight: 600;
186
+ }
187
+
188
+ .warning-box {
189
+ background: #f0f9ff;
190
+ border: 1px solid #bae6fd;
191
+ border-radius: 0.5rem;
192
+ padding: 1rem;
193
+ margin-bottom: 1.5rem;
194
+ text-align: center;
195
+ }
196
+
197
+ .warning-box p {
198
+ margin-bottom: 0.5rem;
199
+ line-height: 1.5;
200
+ color: #6b7280;
201
+ font-size: 0.9375rem;
202
+ }
203
+
204
+ .warning-box p:last-child {
205
+ margin-bottom: 0;
206
+ }
207
+
208
+ .warning-box strong {
209
+ color: #0ea5e9;
210
+ font-weight: 600;
211
+ }
212
+
213
+ .warning-box a {
214
+ color: #0ea5e9;
215
+ text-decoration: underline;
216
+ font-weight: 600;
217
+ }
218
+
219
+ .warning-box a:hover {
220
+ color: #0284c7;
221
+ text-decoration: underline;
222
+ }
223
+ """
224
+
225
+ # Status message styles (for success/error indicators)
226
+ STATUS_MESSAGE_STYLES = """
227
+ .status-message {
228
+ display: flex;
229
+ align-items: center;
230
+ justify-content: center;
231
+ gap: 0.75rem;
232
+ margin-bottom: 1.5rem;
233
+ }
234
+
235
+ .status-icon {
236
+ font-size: 1.5rem;
237
+ line-height: 1;
238
+ display: inline-flex;
239
+ align-items: center;
240
+ justify-content: center;
241
+ width: 2rem;
242
+ height: 2rem;
243
+ border-radius: 0.5rem;
244
+ flex-shrink: 0;
245
+ }
246
+
247
+ .status-icon.success {
248
+ background: #10b98120;
249
+ }
250
+
251
+ .status-icon.error {
252
+ background: #ef444420;
253
+ }
254
+
255
+ .message {
256
+ font-size: 1.125rem;
257
+ line-height: 1.75;
258
+ color: #111827;
259
+ font-weight: 600;
260
+ text-align: left;
261
+ }
262
+ """
263
+
264
+ # Detail box styles (for key-value pairs)
265
+ DETAIL_BOX_STYLES = """
266
+ .detail-box {
267
+ background: #f9fafb;
268
+ border: 1px solid #e5e7eb;
269
+ border-radius: 0.5rem;
270
+ padding: 1rem;
271
+ margin-bottom: 1.5rem;
272
+ text-align: left;
273
+ }
274
+
275
+ .detail-row {
276
+ display: flex;
277
+ padding: 0.5rem 0;
278
+ border-bottom: 1px solid #e5e7eb;
279
+ }
280
+
281
+ .detail-row:last-child {
282
+ border-bottom: none;
283
+ }
284
+
285
+ .detail-label {
286
+ font-weight: 600;
287
+ min-width: 160px;
288
+ color: #6b7280;
289
+ font-size: 0.875rem;
290
+ flex-shrink: 0;
291
+ padding-right: 1rem;
292
+ }
293
+
294
+ .detail-value {
295
+ flex: 1;
296
+ font-family: 'SF Mono', 'Monaco', 'Consolas', 'Courier New', monospace;
297
+ font-size: 0.75rem;
298
+ color: #111827;
299
+ word-break: break-all;
300
+ overflow-wrap: break-word;
301
+ }
302
+ """
303
+
304
+ # Redirect section styles (for OAuth redirect URI box)
305
+ REDIRECT_SECTION_STYLES = """
306
+ .redirect-section {
307
+ background: #fffbeb;
308
+ border: 1px solid #fcd34d;
309
+ border-radius: 0.5rem;
310
+ padding: 1rem;
311
+ margin-bottom: 1.5rem;
312
+ text-align: left;
313
+ }
314
+
315
+ .redirect-section .label {
316
+ font-size: 0.875rem;
317
+ color: #6b7280;
318
+ font-weight: 600;
319
+ margin-bottom: 0.5rem;
320
+ display: block;
321
+ }
322
+
323
+ .redirect-section .value {
324
+ font-family: 'SF Mono', 'Monaco', 'Consolas', 'Courier New', monospace;
325
+ font-size: 0.875rem;
326
+ color: #111827;
327
+ word-break: break-all;
328
+ margin-top: 0.25rem;
329
+ }
330
+ """
331
+
332
+ # Collapsible details styles
333
+ DETAILS_STYLES = """
334
+ details {
335
+ margin-bottom: 1.5rem;
336
+ text-align: left;
337
+ }
338
+
339
+ summary {
340
+ cursor: pointer;
341
+ font-size: 0.875rem;
342
+ color: #6b7280;
343
+ font-weight: 600;
344
+ list-style: none;
345
+ padding: 0.5rem;
346
+ border-radius: 0.25rem;
347
+ }
348
+
349
+ summary:hover {
350
+ background: #f9fafb;
351
+ }
352
+
353
+ summary::marker {
354
+ display: none;
355
+ }
356
+
357
+ summary::before {
358
+ content: "▶";
359
+ display: inline-block;
360
+ margin-right: 0.5rem;
361
+ transition: transform 0.2s;
362
+ font-size: 0.75rem;
363
+ }
364
+
365
+ details[open] summary::before {
366
+ transform: rotate(90deg);
367
+ }
368
+ """
369
+
370
+ # Helper text styles
371
+ HELPER_TEXT_STYLES = """
372
+ .close-instruction, .help-text {
373
+ font-size: 0.875rem;
374
+ color: #6b7280;
375
+ margin-top: 1.5rem;
376
+ }
377
+ """
378
+
379
+ # Tooltip styles for hover help
380
+ TOOLTIP_STYLES = """
381
+ .help-link-container {
382
+ position: fixed;
383
+ bottom: 1.5rem;
384
+ right: 1.5rem;
385
+ font-size: 0.875rem;
386
+ }
387
+
388
+ .help-link {
389
+ color: #6b7280;
390
+ text-decoration: none;
391
+ cursor: help;
392
+ position: relative;
393
+ display: inline-block;
394
+ border-bottom: 1px dotted #9ca3af;
395
+ }
396
+
397
+ @media (max-width: 640px) {
398
+ .help-link {
399
+ background: #ffffff;
400
+ padding: 0.25rem 0.5rem;
401
+ border-radius: 0.25rem;
402
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
403
+ }
404
+ }
405
+
406
+ .help-link:hover {
407
+ color: #111827;
408
+ border-bottom-color: #111827;
409
+ }
410
+
411
+ .help-link:hover .tooltip {
412
+ opacity: 1;
413
+ visibility: visible;
414
+ }
415
+
416
+ .tooltip {
417
+ position: absolute;
418
+ bottom: 100%;
419
+ right: 0;
420
+ left: auto;
421
+ margin-bottom: 0.5rem;
422
+ background: #1f2937;
423
+ color: #ffffff;
424
+ padding: 0.75rem 1rem;
425
+ border-radius: 0.5rem;
426
+ font-size: 0.8125rem;
427
+ line-height: 1.5;
428
+ width: 280px;
429
+ max-width: calc(100vw - 3rem);
430
+ opacity: 0;
431
+ visibility: hidden;
432
+ transition: opacity 0.2s, visibility 0.2s;
433
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
434
+ text-align: left;
435
+ }
436
+
437
+ .tooltip::after {
438
+ content: '';
439
+ position: absolute;
440
+ top: 100%;
441
+ right: 1rem;
442
+ border: 6px solid transparent;
443
+ border-top-color: #1f2937;
444
+ }
445
+
446
+ .tooltip-link {
447
+ color: #60a5fa;
448
+ text-decoration: underline;
449
+ }
450
+ """
451
+
452
+
453
+ def create_page(
454
+ content: str,
455
+ title: str = "FastMCP",
456
+ additional_styles: str = "",
457
+ csp_policy: str = "default-src 'none'; style-src 'unsafe-inline'; img-src https: data:; base-uri 'none'",
458
+ ) -> str:
459
+ """
460
+ Create a complete HTML page with FastMCP styling.
461
+
462
+ Args:
463
+ content: HTML content to place inside the page
464
+ title: Page title
465
+ additional_styles: Extra CSS to include
466
+ csp_policy: Content Security Policy header value.
467
+ If empty string "", the CSP meta tag is omitted entirely.
468
+
469
+ Returns:
470
+ Complete HTML page as string
471
+ """
472
+ title = html.escape(title)
473
+
474
+ # Only include CSP meta tag if policy is non-empty
475
+ csp_meta = (
476
+ f'<meta http-equiv="Content-Security-Policy" content="{html.escape(csp_policy, quote=True)}" />'
477
+ if csp_policy
478
+ else ""
479
+ )
480
+
481
+ return f"""
482
+ <!DOCTYPE html>
483
+ <html lang="en">
484
+ <head>
485
+ <meta charset="UTF-8">
486
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
487
+ <title>{title}</title>
488
+ <style>
489
+ {BASE_STYLES}
490
+ {additional_styles}
491
+ </style>
492
+ {csp_meta}
493
+ </head>
494
+ <body>
495
+ {content}
496
+ </body>
497
+ </html>
498
+ """
499
+
500
+
501
+ def create_logo(icon_url: str | None = None, alt_text: str = "FastMCP") -> str:
502
+ """Create logo HTML.
503
+
504
+ Args:
505
+ icon_url: Optional custom icon URL. If not provided, uses the FastMCP logo.
506
+ alt_text: Alt text for the logo image.
507
+
508
+ Returns:
509
+ HTML for logo image tag.
510
+ """
511
+ url = icon_url or FASTMCP_LOGO_URL
512
+ alt = html.escape(alt_text)
513
+ return f'<img src="{html.escape(url)}" alt="{alt}" class="logo" />'
514
+
515
+
516
+ def create_status_message(message: str, is_success: bool = True) -> str:
517
+ """
518
+ Create a status message with icon.
519
+
520
+ Args:
521
+ message: Status message text
522
+ is_success: True for success (✓), False for error (✕)
523
+
524
+ Returns:
525
+ HTML for status message
526
+ """
527
+ message = html.escape(message)
528
+ icon = "✓" if is_success else "✕"
529
+ icon_class = "success" if is_success else "error"
530
+
531
+ return f"""
532
+ <div class="status-message">
533
+ <span class="status-icon {icon_class}">{icon}</span>
534
+ <div class="message">{message}</div>
535
+ </div>
536
+ """
537
+
538
+
539
+ def create_info_box(
540
+ content: str,
541
+ is_error: bool = False,
542
+ centered: bool = False,
543
+ monospace: bool = False,
544
+ ) -> str:
545
+ """
546
+ Create an info box.
547
+
548
+ Args:
549
+ content: HTML content for the info box
550
+ is_error: True for error styling, False for normal
551
+ centered: True to center the text, False for left-aligned
552
+ monospace: True to use gray monospace font styling instead of blue
553
+
554
+ Returns:
555
+ HTML for info box
556
+ """
557
+ content = html.escape(content)
558
+ base_class = "info-box-mono" if monospace else "info-box"
559
+ classes = [base_class]
560
+ if is_error:
561
+ classes.append("error")
562
+ if centered:
563
+ classes.append("centered")
564
+ class_str = " ".join(classes)
565
+ return f'<div class="{class_str}">{content}</div>'
566
+
567
+
568
+ def create_detail_box(rows: list[tuple[str, str]]) -> str:
569
+ """
570
+ Create a detail box with key-value pairs.
571
+
572
+ Args:
573
+ rows: List of (label, value) tuples
574
+
575
+ Returns:
576
+ HTML for detail box
577
+ """
578
+ rows_html = "\n".join(
579
+ f"""
580
+ <div class="detail-row">
581
+ <div class="detail-label">{html.escape(label)}:</div>
582
+ <div class="detail-value">{html.escape(value)}</div>
583
+ </div>
584
+ """
585
+ for label, value in rows
586
+ )
587
+
588
+ return f'<div class="detail-box">{rows_html}</div>'
589
+
590
+
591
+ def create_button_group(buttons: list[tuple[str, str, str]]) -> str:
592
+ """
593
+ Create a group of buttons.
594
+
595
+ Args:
596
+ buttons: List of (text, value, css_class) tuples
597
+
598
+ Returns:
599
+ HTML for button group
600
+ """
601
+ buttons_html = "\n".join(
602
+ f'<button type="submit" name="action" value="{value}" class="{css_class}">{text}</button>'
603
+ for text, value, css_class in buttons
604
+ )
605
+
606
+ return f'<div class="button-group">{buttons_html}</div>'
607
+
608
+
609
+ def create_secure_html_response(html: str, status_code: int = 200) -> HTMLResponse:
610
+ """
611
+ Create an HTMLResponse with security headers.
612
+
613
+ Adds X-Frame-Options: DENY to prevent clickjacking attacks per MCP security best practices.
614
+
615
+ Args:
616
+ html: HTML content to return
617
+ status_code: HTTP status code
618
+
619
+ Returns:
620
+ HTMLResponse with security headers
621
+ """
622
+ return HTMLResponse(
623
+ content=html,
624
+ status_code=status_code,
625
+ headers={"X-Frame-Options": "DENY"},
626
+ )