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,560 @@
1
+ <div class="mcp-connections-container">
2
+
3
+ <%= link_to "← Back to Connections", mcp_connections_path, class: "mcp-btn mcp-btn-secondary" %>
4
+ <div class="mcp-header">
5
+ <h1><%= @credential.name %></h1>
6
+ <div class="mcp-header-meta">
7
+ <code class="mcp-code"><%= @credential.server_url %></code>
8
+ <% if @credential.expired? %>
9
+ <span class="mcp-badge mcp-badge-danger">Expired</span>
10
+ <% elsif @credential.expires_soon? %>
11
+ <span class="mcp-badge mcp-badge-warning">Expiring Soon</span>
12
+ <% else %>
13
+ <span class="mcp-badge mcp-badge-success">Active</span>
14
+ <% end %>
15
+ </div>
16
+ </div>
17
+
18
+ <% if flash[:notice] %>
19
+ <div class="mcp-alert mcp-alert-success">
20
+ <%= flash[:notice] %>
21
+ </div>
22
+ <% end %>
23
+
24
+ <% if flash[:alert] %>
25
+ <div class="mcp-alert mcp-alert-warning">
26
+ <%= flash[:alert] %>
27
+ </div>
28
+ <% end %>
29
+
30
+ <div class="mcp-tools-section">
31
+ <h2>Available Tools (<%= @tools.count %>)</h2>
32
+ <% if @tools.any? %>
33
+ <div class="mcp-tools-list">
34
+ <% @tools.each do |tool| %>
35
+ <div class="mcp-tool-card">
36
+ <div class="mcp-tool-header">
37
+ <h3 class="mcp-tool-name"><%= tool.name %></h3>
38
+ <div class="mcp-tool-badges">
39
+ <% if tool.annotations&.read_only_hint %>
40
+ <span class="mcp-badge mcp-badge-info">Read Only</span>
41
+ <% end %>
42
+ <% if tool.annotations&.destructive_hint %>
43
+ <span class="mcp-badge mcp-badge-warning">Destructive</span>
44
+ <% end %>
45
+ <% if tool.annotations&.idempotent_hint %>
46
+ <span class="mcp-badge mcp-badge-success">Idempotent</span>
47
+ <% end %>
48
+ </div>
49
+ </div>
50
+
51
+ <% if tool.description.present? %>
52
+ <p class="mcp-tool-description"><%= tool.description %></p>
53
+ <% else %>
54
+ <p class="mcp-tool-description mcp-text-muted">No description available</p>
55
+ <% end %>
56
+
57
+ <% if tool.params_schema.present? && tool.params_schema["properties"]&.any? %>
58
+ <div class="mcp-tool-schema">
59
+ <h4>Input Parameters</h4>
60
+ <div class="mcp-parameters">
61
+ <% tool.params_schema["properties"].each do |param_name, param_info| %>
62
+ <div class="mcp-parameter">
63
+ <div class="mcp-parameter-header">
64
+ <code class="mcp-param-name"><%= param_name %></code>
65
+ <span class="mcp-param-type"><%= param_info["type"] %></span>
66
+ <% if tool.params_schema["required"]&.include?(param_name) %>
67
+ <span class="mcp-badge mcp-badge-required">Required</span>
68
+ <% end %>
69
+ </div>
70
+ <% if param_info["description"].present? %>
71
+ <p class="mcp-parameter-description"><%= param_info["description"] %></p>
72
+ <% end %>
73
+ <% if param_info["items"].present? %>
74
+ <div class="mcp-parameter-items">
75
+ <small class="mcp-text-muted">Items: <%= param_info["items"]["type"] %></small>
76
+ </div>
77
+ <% end %>
78
+ </div>
79
+ <% end %>
80
+ </div>
81
+
82
+ <details class="mcp-schema-details">
83
+ <summary>View Full Schema</summary>
84
+ <pre class="mcp-schema-code"><%= JSON.pretty_generate(tool.params_schema) %></pre>
85
+ </details>
86
+ </div>
87
+ <% elsif tool.params_schema.present? %>
88
+ <div class="mcp-tool-schema">
89
+ <p class="mcp-text-muted"><em>No input parameters required</em></p>
90
+ </div>
91
+ <% end %>
92
+ </div>
93
+ <% end %>
94
+ </div>
95
+ <% else %>
96
+ <div class="mcp-alert mcp-alert-info">
97
+ <p>No tools available from this MCP server.</p>
98
+ </div>
99
+ <% end %>
100
+ </div>
101
+ </div>
102
+
103
+ <style>
104
+ /* MCP Tools Show Page - Self-contained styles */
105
+ .mcp-connections-container {
106
+ max-width: 1200px;
107
+ margin: 2rem auto;
108
+ padding: 0 1rem;
109
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
110
+ line-height: 1.6;
111
+ }
112
+
113
+ .mcp-header {
114
+ margin: 2rem 0;
115
+ }
116
+
117
+ .mcp-header h1 {
118
+ font-size: 2rem;
119
+ font-weight: 600;
120
+ margin-bottom: 0.5rem;
121
+ color: #1a1a1a;
122
+ }
123
+
124
+ .mcp-header-meta {
125
+ display: flex;
126
+ align-items: center;
127
+ gap: 1rem;
128
+ flex-wrap: wrap;
129
+ margin-top: 0.5rem;
130
+ }
131
+
132
+ .mcp-tools-section {
133
+ margin-top: 2rem;
134
+ }
135
+
136
+ .mcp-tools-section h2 {
137
+ font-size: 1.5rem;
138
+ font-weight: 600;
139
+ margin-bottom: 1.5rem;
140
+ color: #2a2a2a;
141
+ }
142
+
143
+ .mcp-tools-list {
144
+ display: flex;
145
+ flex-direction: column;
146
+ gap: 1.5rem;
147
+ margin-top: 1.5rem;
148
+ }
149
+
150
+ .mcp-tool-card {
151
+ background: white;
152
+ border: 1px solid #dee2e6;
153
+ border-radius: 8px;
154
+ padding: 1.5rem;
155
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
156
+ transition: box-shadow 0.2s ease;
157
+ }
158
+
159
+ .mcp-tool-card:hover {
160
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
161
+ }
162
+
163
+ .mcp-tool-header {
164
+ margin-bottom: 1rem;
165
+ }
166
+
167
+ .mcp-tool-name {
168
+ font-size: 1.25rem;
169
+ font-weight: 600;
170
+ color: #007bff;
171
+ margin: 0 0 0.5rem 0;
172
+ word-break: break-word;
173
+ }
174
+
175
+ .mcp-tool-badges {
176
+ display: flex;
177
+ gap: 0.5rem;
178
+ flex-wrap: wrap;
179
+ margin-top: 0.5rem;
180
+ }
181
+
182
+ .mcp-tool-description {
183
+ font-size: 0.95rem;
184
+ color: #495057;
185
+ margin-bottom: 1rem;
186
+ line-height: 1.5;
187
+ }
188
+
189
+ .mcp-tool-schema {
190
+ margin-top: 1rem;
191
+ padding-top: 1rem;
192
+ border-top: 1px solid #dee2e6;
193
+ }
194
+
195
+ .mcp-tool-schema h4 {
196
+ font-size: 0.875rem;
197
+ font-weight: 600;
198
+ color: #6c757d;
199
+ text-transform: uppercase;
200
+ letter-spacing: 0.05em;
201
+ margin-bottom: 0.5rem;
202
+ }
203
+
204
+ .mcp-schema-details {
205
+ margin-top: 0.5rem;
206
+ }
207
+
208
+ .mcp-schema-details summary {
209
+ cursor: pointer;
210
+ font-size: 0.875rem;
211
+ color: #007bff;
212
+ padding: 0.5rem;
213
+ background-color: #f8f9fa;
214
+ border-radius: 4px;
215
+ user-select: none;
216
+ }
217
+
218
+ .mcp-schema-details summary:hover {
219
+ background-color: #e9ecef;
220
+ }
221
+
222
+ .mcp-schema-code {
223
+ margin-top: 0.5rem;
224
+ padding: 1rem;
225
+ background-color: #f5f5f5;
226
+ border: 1px solid #dee2e6;
227
+ border-radius: 4px;
228
+ overflow-x: auto;
229
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
230
+ font-size: 0.875rem;
231
+ line-height: 1.5;
232
+ color: #212529;
233
+ }
234
+
235
+ .mcp-parameters {
236
+ display: flex;
237
+ flex-direction: column;
238
+ gap: 1rem;
239
+ margin-bottom: 1rem;
240
+ }
241
+
242
+ .mcp-parameter {
243
+ padding: 0.75rem;
244
+ background-color: #f8f9fa;
245
+ border-radius: 4px;
246
+ border-left: 3px solid #007bff;
247
+ }
248
+
249
+ .mcp-parameter-header {
250
+ display: flex;
251
+ align-items: center;
252
+ gap: 0.5rem;
253
+ margin-bottom: 0.5rem;
254
+ flex-wrap: wrap;
255
+ }
256
+
257
+ .mcp-param-name {
258
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
259
+ font-size: 0.9rem;
260
+ font-weight: 600;
261
+ color: #007bff;
262
+ background-color: #e7f3ff;
263
+ padding: 0.25rem 0.5rem;
264
+ border-radius: 3px;
265
+ }
266
+
267
+ .mcp-param-type {
268
+ font-size: 0.75rem;
269
+ color: #6c757d;
270
+ background-color: #e9ecef;
271
+ padding: 0.25rem 0.5rem;
272
+ border-radius: 3px;
273
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
274
+ }
275
+
276
+ .mcp-parameter-description {
277
+ font-size: 0.875rem;
278
+ color: #495057;
279
+ margin: 0;
280
+ line-height: 1.5;
281
+ }
282
+
283
+ .mcp-parameter-items {
284
+ margin-top: 0.25rem;
285
+ }
286
+
287
+ /* Badge styles */
288
+ .mcp-badge {
289
+ display: inline-block;
290
+ padding: 0.25rem 0.5rem;
291
+ font-size: 0.75rem;
292
+ font-weight: 600;
293
+ line-height: 1;
294
+ text-align: center;
295
+ white-space: nowrap;
296
+ border-radius: 4px;
297
+ }
298
+
299
+ .mcp-badge-success {
300
+ background-color: #d4edda;
301
+ color: #155724;
302
+ }
303
+
304
+ .mcp-badge-warning {
305
+ background-color: #fff3cd;
306
+ color: #856404;
307
+ }
308
+
309
+ .mcp-badge-danger {
310
+ background-color: #f8d7da;
311
+ color: #721c24;
312
+ }
313
+
314
+ .mcp-badge-info {
315
+ background-color: #d1ecf1;
316
+ color: #0c5460;
317
+ }
318
+
319
+ .mcp-badge-required {
320
+ background-color: #f8d7da;
321
+ color: #721c24;
322
+ font-size: 0.7rem;
323
+ }
324
+
325
+ /* Button styles */
326
+ .mcp-btn {
327
+ display: inline-block;
328
+ padding: 0.5rem 1rem;
329
+ font-size: 0.875rem;
330
+ font-weight: 500;
331
+ line-height: 1.5;
332
+ text-align: center;
333
+ text-decoration: none;
334
+ border: 1px solid transparent;
335
+ border-radius: 4px;
336
+ cursor: pointer;
337
+ transition: all 0.15s ease-in-out;
338
+ }
339
+
340
+ .mcp-btn:hover {
341
+ opacity: 0.9;
342
+ }
343
+
344
+ .mcp-btn-secondary {
345
+ color: white;
346
+ background-color: #6c757d;
347
+ border-color: #6c757d;
348
+ }
349
+
350
+ .mcp-btn-secondary:hover {
351
+ background-color: #5a6268;
352
+ border-color: #5a6268;
353
+ }
354
+
355
+ /* Code styles */
356
+ .mcp-code {
357
+ background-color: #f5f5f5;
358
+ padding: 0.25rem 0.5rem;
359
+ border-radius: 3px;
360
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
361
+ font-size: 0.875em;
362
+ color: #212529;
363
+ }
364
+
365
+ /* Text utilities */
366
+ .mcp-text-muted {
367
+ color: #6c757d;
368
+ }
369
+
370
+ /* Alert styles */
371
+ .mcp-alert {
372
+ padding: 1rem;
373
+ margin-bottom: 1rem;
374
+ border: 1px solid transparent;
375
+ border-radius: 4px;
376
+ }
377
+
378
+ .mcp-alert-info {
379
+ background-color: #d1ecf1;
380
+ border-color: #bee5eb;
381
+ color: #0c5460;
382
+ }
383
+
384
+ .mcp-alert-warning {
385
+ background-color: #fff3cd;
386
+ border-color: #ffeaa7;
387
+ color: #856404;
388
+ }
389
+
390
+ .mcp-alert-success {
391
+ background-color: #d4edda;
392
+ border-color: #c3e6cb;
393
+ color: #155724;
394
+ }
395
+
396
+ /* Responsive */
397
+ @media (max-width: 768px) {
398
+ .mcp-connections-container {
399
+ padding: 0 0.5rem;
400
+ }
401
+
402
+ .mcp-header h1 {
403
+ font-size: 1.5rem;
404
+ }
405
+
406
+ .mcp-tool-card {
407
+ padding: 1rem;
408
+ }
409
+ }
410
+
411
+ /* Dark Mode Support */
412
+ @media (prefers-color-scheme: dark) {
413
+ body {
414
+ background-color: #1a1a1a;
415
+ }
416
+
417
+ .mcp-connections-container {
418
+ color: #e0e0e0;
419
+ background-color: #1a1a1a;
420
+ }
421
+
422
+ .mcp-header h1 {
423
+ color: #f0f0f0;
424
+ }
425
+
426
+ .mcp-tools-section h2 {
427
+ color: #f0f0f0;
428
+ }
429
+
430
+ /* Tool card dark mode */
431
+ .mcp-tool-card {
432
+ background: #2a2a2a;
433
+ border-color: #3a3a3a;
434
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
435
+ }
436
+
437
+ .mcp-tool-card:hover {
438
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.4);
439
+ }
440
+
441
+ .mcp-tool-name {
442
+ color: #4dabf7;
443
+ }
444
+
445
+ .mcp-tool-description {
446
+ color: #b0b0b0;
447
+ }
448
+
449
+ /* Tool schema dark mode */
450
+ .mcp-tool-schema {
451
+ border-top-color: #3a3a3a;
452
+ }
453
+
454
+ .mcp-tool-schema h4 {
455
+ color: #8a8a8a;
456
+ }
457
+
458
+ .mcp-schema-details summary {
459
+ background-color: #1a1a1a;
460
+ color: #4dabf7;
461
+ }
462
+
463
+ .mcp-schema-details summary:hover {
464
+ background-color: #333333;
465
+ }
466
+
467
+ .mcp-schema-code {
468
+ background-color: #1a1a1a;
469
+ border-color: #3a3a3a;
470
+ color: #e0e0e0;
471
+ }
472
+
473
+ /* Parameter dark mode */
474
+ .mcp-parameter {
475
+ background-color: #1a1a1a;
476
+ border-left-color: #4dabf7;
477
+ }
478
+
479
+ .mcp-param-name {
480
+ color: #4dabf7;
481
+ background-color: #0d3a5c;
482
+ }
483
+
484
+ .mcp-param-type {
485
+ color: #8a8a8a;
486
+ background-color: #2a2a2a;
487
+ }
488
+
489
+ .mcp-parameter-description {
490
+ color: #b0b0b0;
491
+ }
492
+
493
+ /* Badge dark mode */
494
+ .mcp-badge-success {
495
+ background-color: #1e4620;
496
+ color: #8fd98f;
497
+ }
498
+
499
+ .mcp-badge-warning {
500
+ background-color: #4a3a00;
501
+ color: #ffd966;
502
+ }
503
+
504
+ .mcp-badge-danger {
505
+ background-color: #4a1a1f;
506
+ color: #ff8a95;
507
+ }
508
+
509
+ .mcp-badge-info {
510
+ background-color: #0c3d47;
511
+ color: #9fe5f0;
512
+ }
513
+
514
+ .mcp-badge-required {
515
+ background-color: #4a1a1f;
516
+ color: #ff8a95;
517
+ }
518
+
519
+ /* Button dark mode */
520
+ .mcp-btn-secondary {
521
+ background-color: #5a6268;
522
+ border-color: #5a6268;
523
+ }
524
+
525
+ .mcp-btn-secondary:hover {
526
+ background-color: #4e555b;
527
+ border-color: #4e555b;
528
+ }
529
+
530
+ /* Code dark mode */
531
+ .mcp-code {
532
+ background-color: #1a1a1a;
533
+ color: #e0e0e0;
534
+ }
535
+
536
+ /* Text utilities dark mode */
537
+ .mcp-text-muted {
538
+ color: #8a8a8a;
539
+ }
540
+
541
+ /* Alert dark mode */
542
+ .mcp-alert-info {
543
+ background-color: #0c3d47;
544
+ border-color: #0f5460;
545
+ color: #9fe5f0;
546
+ }
547
+
548
+ .mcp-alert-warning {
549
+ background-color: #4a3a00;
550
+ border-color: #665000;
551
+ color: #ffd966;
552
+ }
553
+
554
+ .mcp-alert-success {
555
+ background-color: #1e4620;
556
+ border-color: #2a5e2c;
557
+ color: #8fd98f;
558
+ }
559
+ }
560
+ </style>
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ module Auth
6
+ module Browser
7
+ # Handles OAuth callback request processing
8
+ # Extracts and validates OAuth parameters from callback requests
9
+ class CallbackHandler
10
+ attr_reader :callback_path, :logger
11
+
12
+ def initialize(callback_path:, logger: nil)
13
+ @callback_path = callback_path
14
+ @logger = logger || MCP.logger
15
+ end
16
+
17
+ # Validate that the request path matches the expected callback path
18
+ # @param path [String] request path
19
+ # @return [Boolean] true if path is valid
20
+ def valid_callback_path?(path)
21
+ uri_path, = path.split("?", 2)
22
+ uri_path == @callback_path
23
+ end
24
+
25
+ # Parse callback parameters from path
26
+ # @param path [String] full request path with query string
27
+ # @param http_server [HttpServer] HTTP server instance for parsing
28
+ # @return [Hash] parsed parameters
29
+ def parse_callback_params(path, http_server)
30
+ _, query_string = path.split("?", 2)
31
+ params = http_server.parse_query_params(query_string || "")
32
+ @logger.debug("Callback params: #{params.keys.join(', ')}")
33
+ params
34
+ end
35
+
36
+ # Extract OAuth parameters from parsed params
37
+ # @param params [Hash] parsed query parameters
38
+ # @return [Hash] OAuth parameters (code, state, error, error_description)
39
+ def extract_oauth_params(params)
40
+ {
41
+ code: params["code"],
42
+ state: params["state"],
43
+ error: params["error"],
44
+ error_description: params["error_description"]
45
+ }
46
+ end
47
+
48
+ # Update result hash with OAuth parameters (thread-safe)
49
+ # @param oauth_params [Hash] OAuth parameters
50
+ # @param result [Hash] result container
51
+ # @param mutex [Mutex] synchronization mutex
52
+ # @param condition [ConditionVariable] wait condition
53
+ def update_result_with_oauth_params(oauth_params, result, mutex, condition)
54
+ mutex.synchronize do
55
+ if oauth_params[:error]
56
+ result[:error] = oauth_params[:error_description] || oauth_params[:error]
57
+ elsif oauth_params[:code] && oauth_params[:state]
58
+ result[:code] = oauth_params[:code]
59
+ result[:state] = oauth_params[:state]
60
+ else
61
+ result[:error] = "Invalid callback: missing code or state parameter"
62
+ end
63
+ result[:completed] = true
64
+ condition.signal
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ module Auth
6
+ module Browser
7
+ # Callback server wrapper for clean shutdown
8
+ # Manages server lifecycle and thread coordination
9
+ class CallbackServer
10
+ def initialize(server, thread, stop_proc)
11
+ @server = server
12
+ @thread = thread
13
+ @stop_proc = stop_proc
14
+ end
15
+
16
+ # Shutdown server and cleanup resources
17
+ # @return [nil] always returns nil
18
+ def shutdown
19
+ @stop_proc.call
20
+ @server.close unless @server.closed?
21
+ @thread.join(5) # Wait max 5 seconds for thread to finish
22
+ rescue StandardError
23
+ # Ignore shutdown errors
24
+ nil
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end