ruby_llm-mcp 0.7.1 → 0.8.0

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/ruby_llm/mcp/{install_generator.rb → install/install_generator.rb} +4 -2
  3. data/lib/generators/ruby_llm/mcp/oauth/install_generator.rb +354 -0
  4. data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/mcp_token_storage.rb.tt +114 -0
  5. data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/user_mcp_oauth_concern.rb.tt +90 -0
  6. data/lib/generators/ruby_llm/mcp/oauth/templates/controllers/mcp_connections_controller.rb.tt +239 -0
  7. data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/cleanup_expired_oauth_states_job.rb.tt +27 -0
  8. data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/example_job.rb.tt +78 -0
  9. data/lib/generators/ruby_llm/mcp/oauth/templates/lib/mcp_client.rb.tt +68 -0
  10. data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_credentials.rb.tt +19 -0
  11. data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_states.rb.tt +21 -0
  12. data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_credential.rb.tt +54 -0
  13. data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_state.rb.tt +30 -0
  14. data/lib/generators/ruby_llm/mcp/oauth/templates/views/index.html.erb +646 -0
  15. data/lib/generators/ruby_llm/mcp/oauth/templates/views/show.html.erb +560 -0
  16. data/lib/ruby_llm/mcp/auth/browser/callback_handler.rb +71 -0
  17. data/lib/ruby_llm/mcp/auth/browser/callback_server.rb +30 -0
  18. data/lib/ruby_llm/mcp/auth/browser/http_server.rb +115 -0
  19. data/lib/ruby_llm/mcp/auth/browser/opener.rb +41 -0
  20. data/lib/ruby_llm/mcp/auth/browser/pages.rb +539 -0
  21. data/lib/ruby_llm/mcp/auth/browser_oauth_provider.rb +254 -0
  22. data/lib/ruby_llm/mcp/auth/client_registrar.rb +170 -0
  23. data/lib/ruby_llm/mcp/auth/discoverer.rb +124 -0
  24. data/lib/ruby_llm/mcp/auth/flows/authorization_code_flow.rb +105 -0
  25. data/lib/ruby_llm/mcp/auth/flows/client_credentials_flow.rb +66 -0
  26. data/lib/ruby_llm/mcp/auth/grant_strategies/authorization_code.rb +31 -0
  27. data/lib/ruby_llm/mcp/auth/grant_strategies/base.rb +31 -0
  28. data/lib/ruby_llm/mcp/auth/grant_strategies/client_credentials.rb +31 -0
  29. data/lib/ruby_llm/mcp/auth/http_response_handler.rb +65 -0
  30. data/lib/ruby_llm/mcp/auth/memory_storage.rb +72 -0
  31. data/lib/ruby_llm/mcp/auth/oauth_provider.rb +226 -0
  32. data/lib/ruby_llm/mcp/auth/security.rb +44 -0
  33. data/lib/ruby_llm/mcp/auth/session_manager.rb +56 -0
  34. data/lib/ruby_llm/mcp/auth/token_manager.rb +236 -0
  35. data/lib/ruby_llm/mcp/auth/url_builder.rb +78 -0
  36. data/lib/ruby_llm/mcp/auth.rb +359 -0
  37. data/lib/ruby_llm/mcp/client.rb +49 -0
  38. data/lib/ruby_llm/mcp/configuration.rb +39 -13
  39. data/lib/ruby_llm/mcp/coordinator.rb +11 -0
  40. data/lib/ruby_llm/mcp/errors.rb +11 -0
  41. data/lib/ruby_llm/mcp/railtie.rb +2 -10
  42. data/lib/ruby_llm/mcp/tool.rb +1 -1
  43. data/lib/ruby_llm/mcp/transport.rb +94 -1
  44. data/lib/ruby_llm/mcp/transports/sse.rb +116 -22
  45. data/lib/ruby_llm/mcp/transports/stdio.rb +4 -3
  46. data/lib/ruby_llm/mcp/transports/streamable_http.rb +81 -79
  47. data/lib/ruby_llm/mcp/version.rb +1 -1
  48. data/lib/ruby_llm/mcp.rb +10 -4
  49. metadata +40 -5
  50. /data/lib/generators/ruby_llm/mcp/{templates → install/templates}/initializer.rb +0 -0
  51. /data/lib/generators/ruby_llm/mcp/{templates → install/templates}/mcps.yml +0 -0
@@ -0,0 +1,646 @@
1
+ <div class="mcp-connections-container">
2
+ <h1>MCP Server Connections</h1>
3
+
4
+ <% if flash[:notice] %>
5
+ <div class="mcp-alert mcp-alert-success">
6
+ <%= flash[:notice] %>
7
+ </div>
8
+ <% end %>
9
+
10
+ <% if flash[:alert] %>
11
+ <div class="mcp-alert mcp-alert-warning">
12
+ <%= flash[:alert] %>
13
+ </div>
14
+ <% end %>
15
+
16
+ <% if @credentials.any? %>
17
+ <h2>Connected Servers</h2>
18
+ <div class="mcp-table-wrapper">
19
+ <table class="mcp-table">
20
+ <thead>
21
+ <tr>
22
+ <th>Name</th>
23
+ <th>Status</th>
24
+ <th>Scopes</th>
25
+ <th>Expires</th>
26
+ <th>Last Refreshed</th>
27
+ <th>Actions</th>
28
+ </tr>
29
+ </thead>
30
+ <tbody>
31
+ <% @credentials.each do |credential| %>
32
+ <tr>
33
+ <td>
34
+ <strong><%= credential.name %></strong><br>
35
+ <small class="mcp-text-muted"><code class="mcp-code"><%= credential.server_url %></code></small>
36
+ </td>
37
+ <td>
38
+ <% if credential.expired? %>
39
+ <span class="mcp-badge mcp-badge-danger">Expired</span>
40
+ <% elsif credential.expires_soon? %>
41
+ <span class="mcp-badge mcp-badge-warning">Expiring Soon</span>
42
+ <% else %>
43
+ <span class="mcp-badge mcp-badge-success">Active</span>
44
+ <% end %>
45
+ </td>
46
+ <td>
47
+ <% if credential.token&.scope %>
48
+ <code class="mcp-code mcp-code-muted"><%= credential.token.scope %></code>
49
+ <% else %>
50
+ <span class="mcp-text-muted">N/A</span>
51
+ <% end %>
52
+ </td>
53
+ <td>
54
+ <% if credential.token_expires_at %>
55
+ <%= time_tag credential.token_expires_at, credential.token_expires_at.to_fs(:short) %>
56
+ <br>
57
+ <small class="mcp-text-muted">
58
+ (<%= time_ago_in_words(credential.token_expires_at) %>
59
+ <%= credential.token_expires_at > Time.current ? "from now" : "ago" %>)
60
+ </small>
61
+ <% else %>
62
+ <span class="mcp-text-muted">Never</span>
63
+ <% end %>
64
+ </td>
65
+ <td>
66
+ <% if credential.last_refreshed_at %>
67
+ <%= time_ago_in_words(credential.last_refreshed_at) %> ago
68
+ <% else %>
69
+ <span class="mcp-text-muted">Never</span>
70
+ <% end %>
71
+ </td>
72
+ <td>
73
+ <div class="mcp-btn-group">
74
+ <%= link_to "View Tools",
75
+ mcp_connection_path(credential),
76
+ class: "mcp-btn mcp-btn-primary" %>
77
+ <%= button_to "Refresh",
78
+ mcp_connection_path(credential),
79
+ method: :patch,
80
+ class: "mcp-btn #{credential.expired? || credential.expires_soon? ? 'mcp-btn-warning' : 'mcp-btn-secondary'}" %>
81
+ <%= button_to "Disconnect",
82
+ mcp_connection_path(credential),
83
+ method: :delete,
84
+ form: { data: { turbo_confirm: "Are you sure you want to disconnect this MCP server?" } },
85
+ class: "mcp-btn mcp-btn-danger" %>
86
+ </div>
87
+ </td>
88
+ </tr>
89
+ <% end %>
90
+ </tbody>
91
+ </table>
92
+ </div>
93
+ <% else %>
94
+ <div class="mcp-alert mcp-alert-info">
95
+ <h4>No MCP Servers Connected</h4>
96
+ <p>Connect to an MCP server to enable AI-powered features in your account.</p>
97
+ </div>
98
+ <% end %>
99
+
100
+ <div class="mcp-card">
101
+ <div class="mcp-card-body">
102
+ <h3>Add New Server</h3>
103
+ <p>Connect to an MCP server to access tools, resources, and AI capabilities.</p>
104
+
105
+ <%= form_with url: mcp_connections_path, method: :post, class: "mcp-form", data: { turbo: false } do |f| %>
106
+ <div class="mcp-form-group">
107
+ <%= label_tag :name, "Server Name", class: "mcp-form-label" %>
108
+ <%= text_field_tag :name, nil,
109
+ placeholder: "My MCP Server",
110
+ required: true,
111
+ class: "mcp-form-input" %>
112
+ <small class="mcp-form-help">A friendly name to identify this server</small>
113
+ </div>
114
+
115
+ <div class="mcp-form-group">
116
+ <%= label_tag :server_url, "MCP Server URL", class: "mcp-form-label" %>
117
+ <%= text_field_tag :server_url, nil,
118
+ placeholder: "https://mcp.example.com/api",
119
+ required: true,
120
+ class: "mcp-form-input" %>
121
+ <small class="mcp-form-help">Enter the full URL of your MCP server</small>
122
+ </div>
123
+
124
+ <div class="mcp-form-group">
125
+ <%= label_tag :scope, "OAuth Scopes (optional)", class: "mcp-form-label" %>
126
+ <%= text_field_tag :scope, "mcp:read mcp:write",
127
+ placeholder: "mcp:read mcp:write",
128
+ class: "mcp-form-input" %>
129
+ <small class="mcp-form-help">Space-separated list of permissions to request</small>
130
+ </div>
131
+
132
+ <%= submit_tag "Connect Server", class: "mcp-btn mcp-btn-primary" %>
133
+ <% end %>
134
+ </div>
135
+ </div>
136
+
137
+ <div class="mcp-info">
138
+ <h3>What is MCP?</h3>
139
+ <p>
140
+ Model Context Protocol (MCP) allows AI models to securely access external
141
+ data and tools. By connecting your account, you grant AI features permission
142
+ to access specific resources on your behalf.
143
+ </p>
144
+
145
+ <h4>Security & Privacy</h4>
146
+ <ul>
147
+ <li>Tokens are encrypted and stored securely</li>
148
+ <li>Each connection is specific to your account</li>
149
+ <li>You can disconnect at any time</li>
150
+ <li>Tokens automatically refresh when possible</li>
151
+ </ul>
152
+ </div>
153
+ </div>
154
+
155
+ <style>
156
+ /* MCP Connections - Self-contained styles */
157
+ .mcp-connections-container {
158
+ max-width: 1200px;
159
+ margin: 2rem auto;
160
+ padding: 0 1rem;
161
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
162
+ line-height: 1.6;
163
+ }
164
+
165
+ .mcp-connections-container h1 {
166
+ font-size: 2rem;
167
+ font-weight: 600;
168
+ margin-bottom: 2rem;
169
+ color: #1a1a1a;
170
+ }
171
+
172
+ .mcp-connections-container h2 {
173
+ font-size: 1.5rem;
174
+ font-weight: 600;
175
+ margin-top: 2rem;
176
+ margin-bottom: 1rem;
177
+ color: #2a2a2a;
178
+ }
179
+
180
+ .mcp-connections-container h3 {
181
+ font-size: 1.25rem;
182
+ font-weight: 600;
183
+ margin-top: 1.5rem;
184
+ margin-bottom: 0.75rem;
185
+ color: #2a2a2a;
186
+ }
187
+
188
+ .mcp-connections-container h4 {
189
+ font-size: 1.1rem;
190
+ font-weight: 600;
191
+ margin-top: 1rem;
192
+ margin-bottom: 0.5rem;
193
+ color: #2a2a2a;
194
+ }
195
+
196
+ /* Table styles */
197
+ .mcp-table-wrapper {
198
+ overflow-x: auto;
199
+ margin-bottom: 2rem;
200
+ border-radius: 8px;
201
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
202
+ }
203
+
204
+ .mcp-table {
205
+ width: 100%;
206
+ border-collapse: collapse;
207
+ background: white;
208
+ }
209
+
210
+ .mcp-table thead {
211
+ background-color: #f8f9fa;
212
+ border-bottom: 2px solid #dee2e6;
213
+ }
214
+
215
+ .mcp-table th {
216
+ padding: 0.75rem;
217
+ text-align: left;
218
+ font-weight: 600;
219
+ font-size: 0.875rem;
220
+ color: #495057;
221
+ text-transform: uppercase;
222
+ letter-spacing: 0.05em;
223
+ }
224
+
225
+ .mcp-table td {
226
+ padding: 0.75rem;
227
+ border-top: 1px solid #dee2e6;
228
+ vertical-align: top;
229
+ }
230
+
231
+ .mcp-table tbody tr:hover {
232
+ background-color: #f8f9fa;
233
+ }
234
+
235
+ /* Badge styles */
236
+ .mcp-badge {
237
+ display: inline-block;
238
+ padding: 0.25rem 0.5rem;
239
+ font-size: 0.75rem;
240
+ font-weight: 600;
241
+ line-height: 1;
242
+ text-align: center;
243
+ white-space: nowrap;
244
+ border-radius: 4px;
245
+ }
246
+
247
+ .mcp-badge-success {
248
+ background-color: #d4edda;
249
+ color: #155724;
250
+ }
251
+
252
+ .mcp-badge-warning {
253
+ background-color: #fff3cd;
254
+ color: #856404;
255
+ }
256
+
257
+ .mcp-badge-danger {
258
+ background-color: #f8d7da;
259
+ color: #721c24;
260
+ }
261
+
262
+ /* Button styles */
263
+ .mcp-btn {
264
+ display: inline-block;
265
+ padding: 0.375rem 0.75rem;
266
+ font-size: 0.875rem;
267
+ font-weight: 500;
268
+ line-height: 1.5;
269
+ text-align: center;
270
+ text-decoration: none;
271
+ border: 1px solid transparent;
272
+ border-radius: 4px;
273
+ cursor: pointer;
274
+ transition: all 0.15s ease-in-out;
275
+ }
276
+
277
+ .mcp-btn:hover {
278
+ opacity: 0.9;
279
+ }
280
+
281
+ .mcp-btn-primary {
282
+ color: white;
283
+ background-color: #007bff;
284
+ border-color: #007bff;
285
+ }
286
+
287
+ .mcp-btn-primary:hover {
288
+ background-color: #0056b3;
289
+ border-color: #0056b3;
290
+ }
291
+
292
+ .mcp-btn-secondary {
293
+ color: white;
294
+ background-color: #6c757d;
295
+ border-color: #6c757d;
296
+ }
297
+
298
+ .mcp-btn-secondary:hover {
299
+ background-color: #5a6268;
300
+ border-color: #5a6268;
301
+ }
302
+
303
+ .mcp-btn-warning {
304
+ color: #212529;
305
+ background-color: #ffc107;
306
+ border-color: #ffc107;
307
+ }
308
+
309
+ .mcp-btn-warning:hover {
310
+ background-color: #e0a800;
311
+ border-color: #e0a800;
312
+ }
313
+
314
+ .mcp-btn-danger {
315
+ color: white;
316
+ background-color: #dc3545;
317
+ border-color: #dc3545;
318
+ }
319
+
320
+ .mcp-btn-danger:hover {
321
+ background-color: #c82333;
322
+ border-color: #c82333;
323
+ }
324
+
325
+ .mcp-btn-group {
326
+ display: flex;
327
+ gap: 0.5rem;
328
+ flex-wrap: wrap;
329
+ }
330
+
331
+ /* Code styles */
332
+ .mcp-code {
333
+ background-color: #f5f5f5;
334
+ padding: 0.125rem 0.375rem;
335
+ border-radius: 3px;
336
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
337
+ font-size: 0.875em;
338
+ color: #212529;
339
+ }
340
+
341
+ .mcp-code-muted {
342
+ color: #6c757d;
343
+ }
344
+
345
+ /* Text utilities */
346
+ .mcp-text-muted {
347
+ color: #6c757d;
348
+ }
349
+
350
+ /* Card styles */
351
+ .mcp-card {
352
+ background: white;
353
+ border: 1px solid #dee2e6;
354
+ border-radius: 8px;
355
+ margin-bottom: 2rem;
356
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
357
+ }
358
+
359
+ .mcp-card-body {
360
+ padding: 1.5rem;
361
+ }
362
+
363
+ /* Alert styles */
364
+ .mcp-alert {
365
+ padding: 1rem;
366
+ margin-bottom: 1rem;
367
+ border: 1px solid transparent;
368
+ border-radius: 4px;
369
+ }
370
+
371
+ .mcp-alert-info {
372
+ background-color: #d1ecf1;
373
+ border-color: #bee5eb;
374
+ color: #0c5460;
375
+ }
376
+
377
+ .mcp-alert-warning {
378
+ background-color: #fff3cd;
379
+ border-color: #ffeaa7;
380
+ color: #856404;
381
+ }
382
+
383
+ .mcp-alert-success {
384
+ background-color: #d4edda;
385
+ border-color: #c3e6cb;
386
+ color: #155724;
387
+ }
388
+
389
+ .mcp-alert h4 {
390
+ margin-top: 0;
391
+ color: inherit;
392
+ }
393
+
394
+ /* Form styles */
395
+ .mcp-form {
396
+ margin-top: 1rem;
397
+ }
398
+
399
+ .mcp-form-group {
400
+ margin-bottom: 1rem;
401
+ }
402
+
403
+ .mcp-form-label {
404
+ display: block;
405
+ margin-bottom: 0.5rem;
406
+ font-weight: 600;
407
+ font-size: 0.875rem;
408
+ color: #2a2a2a;
409
+ }
410
+
411
+ .mcp-form-input {
412
+ display: block;
413
+ width: 100%;
414
+ padding: 0.5rem 0.75rem;
415
+ font-size: 1rem;
416
+ line-height: 1.5;
417
+ color: #212529;
418
+ background-color: white;
419
+ border: 1px solid #ced4da;
420
+ border-radius: 4px;
421
+ transition: border-color 0.15s ease-in-out;
422
+ }
423
+
424
+ .mcp-form-input:focus {
425
+ outline: 0;
426
+ border-color: #007bff;
427
+ box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
428
+ }
429
+
430
+ .mcp-form-help {
431
+ display: block;
432
+ margin-top: 0.25rem;
433
+ font-size: 0.875rem;
434
+ color: #6c757d;
435
+ }
436
+
437
+ .mcp-form .mcp-btn {
438
+ margin-top: 0.5rem;
439
+ }
440
+
441
+ /* Info section */
442
+ .mcp-info {
443
+ margin-top: 2rem;
444
+ padding: 1.5rem;
445
+ background-color: #f8f9fa;
446
+ border-radius: 8px;
447
+ }
448
+
449
+ .mcp-info ul {
450
+ margin: 0.5rem 0;
451
+ padding-left: 1.5rem;
452
+ }
453
+
454
+ .mcp-info li {
455
+ margin-bottom: 0.25rem;
456
+ }
457
+
458
+ /* Responsive */
459
+ @media (max-width: 768px) {
460
+ .mcp-connections-container {
461
+ padding: 0 0.5rem;
462
+ }
463
+
464
+ .mcp-table {
465
+ font-size: 0.875rem;
466
+ }
467
+
468
+ .mcp-table th,
469
+ .mcp-table td {
470
+ padding: 0.5rem;
471
+ }
472
+
473
+ .mcp-btn-group {
474
+ flex-direction: column;
475
+ }
476
+
477
+ .mcp-btn {
478
+ width: 100%;
479
+ }
480
+ }
481
+
482
+ /* Dark Mode Support */
483
+ @media (prefers-color-scheme: dark) {
484
+ body {
485
+ background-color: #1a1a1a;
486
+ }
487
+
488
+ .mcp-connections-container {
489
+ color: #e0e0e0;
490
+ background-color: #1a1a1a;
491
+ }
492
+
493
+ .mcp-connections-container h1,
494
+ .mcp-connections-container h2,
495
+ .mcp-connections-container h3,
496
+ .mcp-connections-container h4 {
497
+ color: #f0f0f0;
498
+ }
499
+
500
+ /* Table dark mode */
501
+ .mcp-table {
502
+ background: #2a2a2a;
503
+ color: #e0e0e0;
504
+ }
505
+
506
+ .mcp-table thead {
507
+ background-color: #1a1a1a;
508
+ border-bottom-color: #3a3a3a;
509
+ }
510
+
511
+ .mcp-table th {
512
+ color: #b0b0b0;
513
+ }
514
+
515
+ .mcp-table td {
516
+ border-top-color: #3a3a3a;
517
+ }
518
+
519
+ .mcp-table tbody tr:hover {
520
+ background-color: #333333;
521
+ }
522
+
523
+ /* Badge dark mode */
524
+ .mcp-badge-success {
525
+ background-color: #1e4620;
526
+ color: #8fd98f;
527
+ }
528
+
529
+ .mcp-badge-warning {
530
+ background-color: #4a3a00;
531
+ color: #ffd966;
532
+ }
533
+
534
+ .mcp-badge-danger {
535
+ background-color: #4a1a1f;
536
+ color: #ff8a95;
537
+ }
538
+
539
+ /* Button dark mode */
540
+ .mcp-btn-primary {
541
+ background-color: #0d6efd;
542
+ border-color: #0d6efd;
543
+ }
544
+
545
+ .mcp-btn-primary:hover {
546
+ background-color: #0a58ca;
547
+ border-color: #0a58ca;
548
+ }
549
+
550
+ .mcp-btn-secondary {
551
+ background-color: #5a6268;
552
+ border-color: #5a6268;
553
+ }
554
+
555
+ .mcp-btn-secondary:hover {
556
+ background-color: #4e555b;
557
+ border-color: #4e555b;
558
+ }
559
+
560
+ .mcp-btn-warning {
561
+ background-color: #e0a800;
562
+ border-color: #e0a800;
563
+ color: #000;
564
+ }
565
+
566
+ .mcp-btn-warning:hover {
567
+ background-color: #c79100;
568
+ border-color: #c79100;
569
+ }
570
+
571
+ .mcp-btn-danger {
572
+ background-color: #bb2d3b;
573
+ border-color: #bb2d3b;
574
+ }
575
+
576
+ .mcp-btn-danger:hover {
577
+ background-color: #a02633;
578
+ border-color: #a02633;
579
+ }
580
+
581
+ /* Code dark mode */
582
+ .mcp-code {
583
+ background-color: #1a1a1a;
584
+ color: #e0e0e0;
585
+ }
586
+
587
+ .mcp-code-muted {
588
+ color: #8a8a8a;
589
+ }
590
+
591
+ /* Text utilities dark mode */
592
+ .mcp-text-muted {
593
+ color: #8a8a8a;
594
+ }
595
+
596
+ /* Card dark mode */
597
+ .mcp-card {
598
+ background: #2a2a2a;
599
+ border-color: #3a3a3a;
600
+ }
601
+
602
+ /* Alert dark mode */
603
+ .mcp-alert-info {
604
+ background-color: #0c3d47;
605
+ border-color: #0f5460;
606
+ color: #9fe5f0;
607
+ }
608
+
609
+ .mcp-alert-warning {
610
+ background-color: #4a3a00;
611
+ border-color: #665000;
612
+ color: #ffd966;
613
+ }
614
+
615
+ .mcp-alert-success {
616
+ background-color: #1e4620;
617
+ border-color: #2a5e2c;
618
+ color: #8fd98f;
619
+ }
620
+
621
+ /* Form dark mode */
622
+ .mcp-form-input {
623
+ background-color: #1a1a1a;
624
+ border-color: #3a3a3a;
625
+ color: #e0e0e0;
626
+ }
627
+
628
+ .mcp-form-input:focus {
629
+ border-color: #0d6efd;
630
+ box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.25);
631
+ }
632
+
633
+ .mcp-form-label {
634
+ color: #d0d0d0;
635
+ }
636
+
637
+ .mcp-form-help {
638
+ color: #8a8a8a;
639
+ }
640
+
641
+ /* Info section dark mode */
642
+ .mcp-info {
643
+ background-color: #1a1a1a;
644
+ }
645
+ }
646
+ </style>