ruby_llm_swarm-mcp 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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +277 -0
- data/lib/generators/ruby_llm/mcp/install/install_generator.rb +42 -0
- data/lib/generators/ruby_llm/mcp/install/templates/initializer.rb +56 -0
- data/lib/generators/ruby_llm/mcp/install/templates/mcps.yml +29 -0
- data/lib/generators/ruby_llm/mcp/oauth/install_generator.rb +354 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/mcp_token_storage.rb.tt +114 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/user_mcp_oauth_concern.rb.tt +90 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/controllers/mcp_connections_controller.rb.tt +239 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/cleanup_expired_oauth_states_job.rb.tt +27 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/example_job.rb.tt +78 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/lib/mcp_client.rb.tt +68 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_credentials.rb.tt +19 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_states.rb.tt +21 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_credential.rb.tt +54 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_state.rb.tt +30 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/views/index.html.erb +646 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/views/show.html.erb +560 -0
- data/lib/ruby_llm/chat.rb +34 -0
- data/lib/ruby_llm/mcp/adapters/base_adapter.rb +179 -0
- data/lib/ruby_llm/mcp/adapters/mcp_sdk_adapter.rb +292 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/coordinator_stub.rb +33 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/sse.rb +52 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/stdio.rb +52 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/streamable_http.rb +86 -0
- data/lib/ruby_llm/mcp/adapters/ruby_llm_adapter.rb +92 -0
- data/lib/ruby_llm/mcp/attachment.rb +18 -0
- data/lib/ruby_llm/mcp/auth/browser/callback_handler.rb +71 -0
- data/lib/ruby_llm/mcp/auth/browser/callback_server.rb +30 -0
- data/lib/ruby_llm/mcp/auth/browser/http_server.rb +112 -0
- data/lib/ruby_llm/mcp/auth/browser/opener.rb +39 -0
- data/lib/ruby_llm/mcp/auth/browser/pages.rb +607 -0
- data/lib/ruby_llm/mcp/auth/browser_oauth_provider.rb +280 -0
- data/lib/ruby_llm/mcp/auth/client_registrar.rb +170 -0
- data/lib/ruby_llm/mcp/auth/discoverer.rb +124 -0
- data/lib/ruby_llm/mcp/auth/flows/authorization_code_flow.rb +105 -0
- data/lib/ruby_llm/mcp/auth/flows/client_credentials_flow.rb +66 -0
- data/lib/ruby_llm/mcp/auth/grant_strategies/authorization_code.rb +31 -0
- data/lib/ruby_llm/mcp/auth/grant_strategies/base.rb +31 -0
- data/lib/ruby_llm/mcp/auth/grant_strategies/client_credentials.rb +31 -0
- data/lib/ruby_llm/mcp/auth/http_response_handler.rb +63 -0
- data/lib/ruby_llm/mcp/auth/memory_storage.rb +90 -0
- data/lib/ruby_llm/mcp/auth/oauth_provider.rb +305 -0
- data/lib/ruby_llm/mcp/auth/security.rb +44 -0
- data/lib/ruby_llm/mcp/auth/session_manager.rb +54 -0
- data/lib/ruby_llm/mcp/auth/token_manager.rb +236 -0
- data/lib/ruby_llm/mcp/auth/transport_oauth_helper.rb +107 -0
- data/lib/ruby_llm/mcp/auth/url_builder.rb +76 -0
- data/lib/ruby_llm/mcp/auth.rb +359 -0
- data/lib/ruby_llm/mcp/client.rb +401 -0
- data/lib/ruby_llm/mcp/completion.rb +16 -0
- data/lib/ruby_llm/mcp/configuration.rb +310 -0
- data/lib/ruby_llm/mcp/content.rb +28 -0
- data/lib/ruby_llm/mcp/elicitation.rb +48 -0
- data/lib/ruby_llm/mcp/error.rb +34 -0
- data/lib/ruby_llm/mcp/errors.rb +91 -0
- data/lib/ruby_llm/mcp/logging.rb +16 -0
- data/lib/ruby_llm/mcp/native/cancellable_operation.rb +57 -0
- data/lib/ruby_llm/mcp/native/client.rb +387 -0
- data/lib/ruby_llm/mcp/native/json_rpc.rb +170 -0
- data/lib/ruby_llm/mcp/native/messages/helpers.rb +39 -0
- data/lib/ruby_llm/mcp/native/messages/notifications.rb +42 -0
- data/lib/ruby_llm/mcp/native/messages/requests.rb +206 -0
- data/lib/ruby_llm/mcp/native/messages/responses.rb +106 -0
- data/lib/ruby_llm/mcp/native/messages.rb +36 -0
- data/lib/ruby_llm/mcp/native/notification.rb +16 -0
- data/lib/ruby_llm/mcp/native/protocol.rb +36 -0
- data/lib/ruby_llm/mcp/native/response_handler.rb +110 -0
- data/lib/ruby_llm/mcp/native/transport.rb +88 -0
- data/lib/ruby_llm/mcp/native/transports/sse.rb +607 -0
- data/lib/ruby_llm/mcp/native/transports/stdio.rb +356 -0
- data/lib/ruby_llm/mcp/native/transports/streamable_http.rb +926 -0
- data/lib/ruby_llm/mcp/native/transports/support/http_client.rb +28 -0
- data/lib/ruby_llm/mcp/native/transports/support/rate_limit.rb +49 -0
- data/lib/ruby_llm/mcp/native/transports/support/timeout.rb +36 -0
- data/lib/ruby_llm/mcp/native.rb +12 -0
- data/lib/ruby_llm/mcp/notification_handler.rb +100 -0
- data/lib/ruby_llm/mcp/progress.rb +35 -0
- data/lib/ruby_llm/mcp/prompt.rb +132 -0
- data/lib/ruby_llm/mcp/railtie.rb +14 -0
- data/lib/ruby_llm/mcp/resource.rb +112 -0
- data/lib/ruby_llm/mcp/resource_template.rb +85 -0
- data/lib/ruby_llm/mcp/result.rb +108 -0
- data/lib/ruby_llm/mcp/roots.rb +45 -0
- data/lib/ruby_llm/mcp/sample.rb +152 -0
- data/lib/ruby_llm/mcp/server_capabilities.rb +49 -0
- data/lib/ruby_llm/mcp/tool.rb +228 -0
- data/lib/ruby_llm/mcp/version.rb +7 -0
- data/lib/ruby_llm/mcp.rb +125 -0
- data/lib/tasks/release.rake +23 -0
- metadata +184 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Auth
|
|
6
|
+
module Browser
|
|
7
|
+
# HTML page generation for OAuth callback responses
|
|
8
|
+
# Provides default success and error pages with customization support
|
|
9
|
+
class Pages
|
|
10
|
+
attr_reader :custom_success_page, :custom_error_page
|
|
11
|
+
|
|
12
|
+
# @param custom_success_page [String, Proc] custom HTML for success page
|
|
13
|
+
# @param custom_error_page [String, Proc] custom HTML for error page (accepts error_message)
|
|
14
|
+
def initialize(custom_success_page: nil, custom_error_page: nil)
|
|
15
|
+
@custom_success_page = custom_success_page
|
|
16
|
+
@custom_error_page = custom_error_page
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Generate success page HTML
|
|
20
|
+
# @return [String] HTML content
|
|
21
|
+
def success_page
|
|
22
|
+
return @custom_success_page.call if @custom_success_page.respond_to?(:call)
|
|
23
|
+
return @custom_success_page if @custom_success_page.is_a?(String)
|
|
24
|
+
|
|
25
|
+
default_success_page
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Generate error page HTML
|
|
29
|
+
# @param error_message [String] error message to display
|
|
30
|
+
# @return [String] HTML content
|
|
31
|
+
def error_page(error_message)
|
|
32
|
+
if @custom_error_page.respond_to?(:call)
|
|
33
|
+
return @custom_error_page.call(error_message)
|
|
34
|
+
elsif @custom_error_page.is_a?(String)
|
|
35
|
+
return @custom_error_page
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
default_error_page(error_message)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# Default HTML success page
|
|
44
|
+
# @return [String] HTML content
|
|
45
|
+
def default_success_page
|
|
46
|
+
<<~HTML
|
|
47
|
+
<!DOCTYPE html>
|
|
48
|
+
<html lang="en">
|
|
49
|
+
<head>
|
|
50
|
+
<meta charset="UTF-8" />
|
|
51
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
52
|
+
<title>RubyLLM MCP — Success</title>
|
|
53
|
+
<link rel="icon" type="image/svg+xml" href="https://www.rubyllm-mcp.com/assets/images/favicon/favicon.svg">
|
|
54
|
+
<link rel="alternate icon" type="image/x-icon" href="https://www.rubyllm-mcp.com/assets/images/favicon/favicon.ico">
|
|
55
|
+
<style>
|
|
56
|
+
:root {
|
|
57
|
+
--ruby-500: #CC342D;
|
|
58
|
+
--ruby-600: #B82E28;
|
|
59
|
+
--green-500: #22C55E;
|
|
60
|
+
--green-600: #16A34A;
|
|
61
|
+
|
|
62
|
+
--text-900: #111827;
|
|
63
|
+
--text-600: #4B5563;
|
|
64
|
+
--card-bg: #f4f3f2;
|
|
65
|
+
--logo-border: rgba(0, 0, 0, 0.5);
|
|
66
|
+
--shadow-xl: 0 25px 50px -12px rgba(0,0,0,0.35);
|
|
67
|
+
--radius-3xl: 1.5rem;
|
|
68
|
+
color-scheme: light dark;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Page background with layered radial gradients (from your React inline style) */
|
|
72
|
+
html, body { height: 100%; }
|
|
73
|
+
body {
|
|
74
|
+
margin: 0;
|
|
75
|
+
display: grid;
|
|
76
|
+
place-items: center;
|
|
77
|
+
padding: 1rem;
|
|
78
|
+
background:
|
|
79
|
+
radial-gradient(at 20% 30%, #8B0000 0%, transparent 50%),
|
|
80
|
+
radial-gradient(at 80% 70%, #FFFFFF 0%, transparent 40%),
|
|
81
|
+
radial-gradient(at 40% 80%, #B22222 0%, transparent 50%),
|
|
82
|
+
radial-gradient(at 60% 20%, #FFE4E4 0%, transparent 45%),
|
|
83
|
+
radial-gradient(at 10% 70%, #1a1a1a 0%, transparent 50%),
|
|
84
|
+
radial-gradient(at 90% 30%, #4a4a4a 0%, transparent 45%),
|
|
85
|
+
linear-gradient(135deg, #CC342D 0%, #E94B3C 50%, #FF6B6B 100%);
|
|
86
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Logo card in top right */
|
|
90
|
+
.logo-card {
|
|
91
|
+
position: fixed;
|
|
92
|
+
top: 1rem;
|
|
93
|
+
right: 1rem;
|
|
94
|
+
background: var(--card-bg);
|
|
95
|
+
border-radius: 0.75rem;
|
|
96
|
+
padding: 0.625rem;
|
|
97
|
+
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
gap: 0.5rem;
|
|
101
|
+
opacity: 0;
|
|
102
|
+
transform: translateY(-20px);
|
|
103
|
+
animation: slide-in 500ms ease-out 200ms forwards;
|
|
104
|
+
}
|
|
105
|
+
.logo {
|
|
106
|
+
width: 32px;
|
|
107
|
+
height: 32px;
|
|
108
|
+
border-radius: 0.375rem;
|
|
109
|
+
border: 1.5px solid var(--logo-border);
|
|
110
|
+
flex-shrink: 0;
|
|
111
|
+
}
|
|
112
|
+
.logo-text {
|
|
113
|
+
font-weight: 600;
|
|
114
|
+
font-size: 0.875rem;
|
|
115
|
+
color: var(--text-900);
|
|
116
|
+
white-space: nowrap;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Main Card */
|
|
120
|
+
.card {
|
|
121
|
+
width: 100%;
|
|
122
|
+
max-width: 32rem;
|
|
123
|
+
background: var(--card-bg);
|
|
124
|
+
color: var(--text-900);
|
|
125
|
+
border-radius: var(--radius-3xl);
|
|
126
|
+
box-shadow: var(--shadow-xl);
|
|
127
|
+
text-align: center;
|
|
128
|
+
padding: 3rem;
|
|
129
|
+
opacity: 0;
|
|
130
|
+
transform: scale(0.8);
|
|
131
|
+
animation: pop-in 500ms ease-out forwards;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* SVG check animation */
|
|
135
|
+
.checkwrap {
|
|
136
|
+
display: flex;
|
|
137
|
+
justify-content: center;
|
|
138
|
+
margin-bottom: 2rem;
|
|
139
|
+
}
|
|
140
|
+
.checkmark { width: 120px; height: 120px; overflow: visible; }
|
|
141
|
+
.circle {
|
|
142
|
+
fill: none;
|
|
143
|
+
stroke: var(--green-600);
|
|
144
|
+
stroke-width: 4;
|
|
145
|
+
opacity: 0;
|
|
146
|
+
stroke-dasharray: 339.292; /* ~2πr for r=54 */
|
|
147
|
+
stroke-dashoffset: 339.292;
|
|
148
|
+
animation: draw-circle 800ms ease-in-out forwards;
|
|
149
|
+
}
|
|
150
|
+
.tick {
|
|
151
|
+
fill: none;
|
|
152
|
+
stroke: var(--green-600);
|
|
153
|
+
stroke-width: 6;
|
|
154
|
+
stroke-linecap: round;
|
|
155
|
+
stroke-linejoin: round;
|
|
156
|
+
opacity: 0;
|
|
157
|
+
stroke-dasharray: 120;
|
|
158
|
+
stroke-dashoffset: 120;
|
|
159
|
+
animation: draw-tick 600ms ease-in-out 500ms forwards;
|
|
160
|
+
}
|
|
161
|
+
@media (prefers-color-scheme: dark) {
|
|
162
|
+
.tick, .circle {
|
|
163
|
+
stroke: var(--green-600);
|
|
164
|
+
stroke-width: 6;
|
|
165
|
+
paint-order: stroke fill;
|
|
166
|
+
stroke-linecap: round;
|
|
167
|
+
stroke-linejoin: round;
|
|
168
|
+
filter: drop-shadow(0 0 2px rgba(255,255,255,0.2));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/* Content entrance */
|
|
172
|
+
.content {
|
|
173
|
+
opacity: 0;
|
|
174
|
+
transform: translateY(20px);
|
|
175
|
+
animation: rise-in 500ms ease-out 800ms forwards;
|
|
176
|
+
}
|
|
177
|
+
h1 {
|
|
178
|
+
margin: 0 0 1rem 0;
|
|
179
|
+
font-size: 2rem;
|
|
180
|
+
line-height: 1.2;
|
|
181
|
+
}
|
|
182
|
+
p { margin: 0 0 2rem 0; color: var(--text-600); }
|
|
183
|
+
|
|
184
|
+
/* Button */
|
|
185
|
+
.btn {
|
|
186
|
+
display: inline-block;
|
|
187
|
+
padding: 0.75rem 2rem;
|
|
188
|
+
border-radius: 9999px;
|
|
189
|
+
background: var(--ruby-500);
|
|
190
|
+
color: #fff;
|
|
191
|
+
border: none;
|
|
192
|
+
cursor: pointer;
|
|
193
|
+
box-shadow: 0 10px 20px rgba(0,0,0,0.15);
|
|
194
|
+
transition: transform 120ms ease, box-shadow 180ms ease, background-color 180ms ease;
|
|
195
|
+
will-change: transform;
|
|
196
|
+
font-weight: 600;
|
|
197
|
+
}
|
|
198
|
+
.btn:hover {
|
|
199
|
+
background: var(--ruby-600);
|
|
200
|
+
transform: scale(1.05);
|
|
201
|
+
box-shadow: 0 14px 28px rgba(0,0,0,0.2);
|
|
202
|
+
}
|
|
203
|
+
.btn:active { transform: scale(0.95); }
|
|
204
|
+
.btn:focus-visible {
|
|
205
|
+
outline: 3px solid rgba(204,52,45,0.35);
|
|
206
|
+
outline-offset: 2px;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* Animations */
|
|
210
|
+
@keyframes pop-in { to { opacity: 1; transform: scale(1); } }
|
|
211
|
+
@keyframes slide-in { to { opacity: 1; transform: translateY(0); } }
|
|
212
|
+
@keyframes draw-circle {
|
|
213
|
+
0% { opacity: 0; stroke-dashoffset: 339.292; }
|
|
214
|
+
20% { opacity: 1; }
|
|
215
|
+
100% { opacity: 1; stroke-dashoffset: 0; }
|
|
216
|
+
}
|
|
217
|
+
@keyframes draw-tick {
|
|
218
|
+
0% { opacity: 0; stroke-dashoffset: 120; }
|
|
219
|
+
30% { opacity: 1; }
|
|
220
|
+
100% { opacity: 1; stroke-dashoffset: 0; }
|
|
221
|
+
}
|
|
222
|
+
@keyframes rise-in { to { opacity: 1; transform: translateY(0); } }
|
|
223
|
+
|
|
224
|
+
/* Motion-reduction respect */
|
|
225
|
+
@media (prefers-reduced-motion: reduce) {
|
|
226
|
+
.card, .logo-card, .circle, .tick, .content { animation: none !important; }
|
|
227
|
+
.card, .logo-card { opacity: 1; transform: none; }
|
|
228
|
+
.content { opacity: 1; transform: none; }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* Responsive adjustments */
|
|
232
|
+
@media (max-width: 640px) {
|
|
233
|
+
.logo-card {
|
|
234
|
+
top: 0.75rem;
|
|
235
|
+
right: 0.75rem;
|
|
236
|
+
padding: 0.5rem;
|
|
237
|
+
gap: 0.375rem;
|
|
238
|
+
}
|
|
239
|
+
.logo { width: 28px; height: 28px; }
|
|
240
|
+
.logo-text { font-size: 0.75rem; }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* =========================
|
|
244
|
+
Dark Mode (automatic)
|
|
245
|
+
========================= */
|
|
246
|
+
@media (prefers-color-scheme: dark) {
|
|
247
|
+
:root {
|
|
248
|
+
--text-900: #F3F4F6; /* near-white */
|
|
249
|
+
--text-600: #D1D5DB; /* soft gray */
|
|
250
|
+
--card-bg: #151417; /* deep neutral to flatter the logo */
|
|
251
|
+
--logo-border: rgba(255, 255, 255, 0.35);
|
|
252
|
+
--shadow-xl: 0 25px 50px -12px rgba(0,0,0,0.7);
|
|
253
|
+
/* brighten the success strokes a touch on dark */
|
|
254
|
+
--green-600: #22C55E; /* use brighter green on dark */
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
body {
|
|
258
|
+
/* Darker background variant keeping brand feel */
|
|
259
|
+
background:
|
|
260
|
+
radial-gradient(at 20% 30%, #560606 0%, transparent 50%),
|
|
261
|
+
radial-gradient(at 80% 70%, #2a2a2a 0%, transparent 40%),
|
|
262
|
+
radial-gradient(at 40% 80%, #6d1414 0%, transparent 50%),
|
|
263
|
+
radial-gradient(at 60% 20%, #3a2a2a 0%, transparent 45%),
|
|
264
|
+
radial-gradient(at 10% 70%, #0e0e0e 0%, transparent 50%),
|
|
265
|
+
radial-gradient(at 90% 30%, #1a1a1a 0%, transparent 45%),
|
|
266
|
+
linear-gradient(135deg, #7e1f1b 0%, #a62b26 50%, #b43b3b 100%);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.btn {
|
|
270
|
+
box-shadow: 0 10px 20px rgba(0,0,0,0.5);
|
|
271
|
+
}
|
|
272
|
+
.btn:hover {
|
|
273
|
+
box-shadow: 0 14px 28px rgba(0,0,0,0.6);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
</style>
|
|
277
|
+
|
|
278
|
+
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#CC342D">
|
|
279
|
+
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#151417" />
|
|
280
|
+
</head>
|
|
281
|
+
<body>
|
|
282
|
+
<div class="logo-card">
|
|
283
|
+
<img
|
|
284
|
+
class="logo"
|
|
285
|
+
src="https://www.rubyllm-mcp.com/assets/images/rubyllm-mcp-logo.svg"
|
|
286
|
+
alt="RubyLLM MCP Logo"
|
|
287
|
+
decoding="async"
|
|
288
|
+
fetchpriority="high"
|
|
289
|
+
/>
|
|
290
|
+
<span class="logo-text">RubyLLM::MCP</span>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
<main class="card" role="dialog" aria-labelledby="title" aria-describedby="desc">
|
|
294
|
+
<div class="checkwrap">
|
|
295
|
+
<svg class="checkmark" viewBox="0 0 120 120" aria-hidden="true">
|
|
296
|
+
<circle class="circle" cx="60" cy="60" r="54"></circle>
|
|
297
|
+
<path class="tick" d="M 35 60 L 52 77 L 85 44"></path>
|
|
298
|
+
</svg>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<div class="content">
|
|
302
|
+
<h1 id="title">Authentication Successful!</h1>
|
|
303
|
+
<p id="desc">You can close this window and return to your application.</p>
|
|
304
|
+
</div>
|
|
305
|
+
</main>
|
|
306
|
+
</body>
|
|
307
|
+
</html>
|
|
308
|
+
HTML
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Default HTML error page
|
|
312
|
+
# @param error_message [String] error message
|
|
313
|
+
# @return [String] HTML content
|
|
314
|
+
def default_error_page(error_message)
|
|
315
|
+
<<~HTML
|
|
316
|
+
<!DOCTYPE html>
|
|
317
|
+
<html lang="en">
|
|
318
|
+
<head>
|
|
319
|
+
<meta charset="UTF-8" />
|
|
320
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
321
|
+
<title>RubyLLM MCP — Authentication Failed</title>
|
|
322
|
+
<link rel="icon" type="image/svg+xml" href="https://www.rubyllm-mcp.com/assets/images/favicon/favicon.svg">
|
|
323
|
+
<link rel="alternate icon" type="image/x-icon" href="https://www.rubyllm-mcp.com/assets/images/favicon/favicon.ico">
|
|
324
|
+
<style>
|
|
325
|
+
:root {
|
|
326
|
+
color-scheme: light dark; /* Hint to the browser UI */
|
|
327
|
+
--ruby-500: #CC342D;
|
|
328
|
+
--ruby-600: #B82E28;
|
|
329
|
+
|
|
330
|
+
--text-900: #111827;
|
|
331
|
+
--text-600: #4B5563;
|
|
332
|
+
--card-bg: #ffffff;
|
|
333
|
+
--logo-border: rgba(0, 0, 0, 0.5);
|
|
334
|
+
|
|
335
|
+
--shadow-xl: 0 25px 50px -12px rgba(0,0,0,0.35);
|
|
336
|
+
--radius-3xl: 1.5rem;
|
|
337
|
+
|
|
338
|
+
--error-bg: #ffebee; /* light mode error panel */
|
|
339
|
+
--error-border: #f44336;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/* Page background (matches Success page) */
|
|
343
|
+
html, body { height: 100%; }
|
|
344
|
+
body {
|
|
345
|
+
margin: 0;
|
|
346
|
+
display: grid;
|
|
347
|
+
place-items: center;
|
|
348
|
+
padding: 1rem;
|
|
349
|
+
background:
|
|
350
|
+
radial-gradient(at 20% 30%, #8B0000 0%, transparent 50%),
|
|
351
|
+
radial-gradient(at 80% 70%, #FFFFFF 0%, transparent 40%),
|
|
352
|
+
radial-gradient(at 40% 80%, #B22222 0%, transparent 50%),
|
|
353
|
+
radial-gradient(at 60% 20%, #FFE4E4 0%, transparent 45%),
|
|
354
|
+
radial-gradient(at 10% 70%, #1a1a1a 0%, transparent 50%),
|
|
355
|
+
radial-gradient(at 90% 30%, #4a4a4a 0%, transparent 45%),
|
|
356
|
+
linear-gradient(135deg, #CC342D 0%, #E94B3C 50%, #FF6B6B 100%);
|
|
357
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/* Logo card in top right */
|
|
361
|
+
.logo-card {
|
|
362
|
+
position: fixed;
|
|
363
|
+
top: 1rem;
|
|
364
|
+
right: 1rem;
|
|
365
|
+
background: var(--card-bg);
|
|
366
|
+
border-radius: 0.75rem;
|
|
367
|
+
padding: 0.625rem;
|
|
368
|
+
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
|
|
369
|
+
display: flex;
|
|
370
|
+
align-items: center;
|
|
371
|
+
gap: 0.5rem;
|
|
372
|
+
opacity: 0;
|
|
373
|
+
transform: translateY(-20px);
|
|
374
|
+
animation: slide-in 500ms ease-out 200ms forwards;
|
|
375
|
+
}
|
|
376
|
+
.logo {
|
|
377
|
+
width: 32px;
|
|
378
|
+
height: 32px;
|
|
379
|
+
border-radius: 0.375rem;
|
|
380
|
+
border: 1.5px solid var(--logo-border);
|
|
381
|
+
flex-shrink: 0;
|
|
382
|
+
}
|
|
383
|
+
.logo-text {
|
|
384
|
+
font-weight: 600;
|
|
385
|
+
font-size: 0.875rem;
|
|
386
|
+
color: var(--text-900);
|
|
387
|
+
white-space: nowrap;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* Main Card */
|
|
391
|
+
.card {
|
|
392
|
+
width: 100%;
|
|
393
|
+
max-width: 32rem;
|
|
394
|
+
background: var(--card-bg);
|
|
395
|
+
color: var(--text-900);
|
|
396
|
+
border-radius: var(--radius-3xl);
|
|
397
|
+
box-shadow: var(--shadow-xl);
|
|
398
|
+
text-align: center;
|
|
399
|
+
padding: 3rem;
|
|
400
|
+
opacity: 0;
|
|
401
|
+
transform: scale(0.8);
|
|
402
|
+
animation: pop-in 500ms ease-out forwards;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/* Animated error icon (circle + X) */
|
|
406
|
+
.iconwrap {
|
|
407
|
+
display: flex;
|
|
408
|
+
justify-content: center;
|
|
409
|
+
margin-bottom: 2rem;
|
|
410
|
+
}
|
|
411
|
+
.erroricon {
|
|
412
|
+
width: 120px;
|
|
413
|
+
height: 120px;
|
|
414
|
+
overflow: visible;
|
|
415
|
+
}
|
|
416
|
+
.circle {
|
|
417
|
+
fill: none;
|
|
418
|
+
stroke: var(--ruby-500);
|
|
419
|
+
stroke-width: 4;
|
|
420
|
+
opacity: 0;
|
|
421
|
+
stroke-dasharray: 339.292; /* 2πr for r=54 */
|
|
422
|
+
stroke-dashoffset: 339.292;
|
|
423
|
+
animation: draw-circle 800ms ease-in-out forwards;
|
|
424
|
+
}
|
|
425
|
+
.x1, .x2 {
|
|
426
|
+
fill: none;
|
|
427
|
+
stroke: var(--ruby-500);
|
|
428
|
+
stroke-width: 6;
|
|
429
|
+
stroke-linecap: round;
|
|
430
|
+
opacity: 0;
|
|
431
|
+
stroke-dasharray: 120;
|
|
432
|
+
stroke-dashoffset: 120;
|
|
433
|
+
}
|
|
434
|
+
.x1 { animation: draw-x 500ms ease-in-out 500ms forwards; }
|
|
435
|
+
.x2 { animation: draw-x 500ms ease-in-out 650ms forwards; }
|
|
436
|
+
|
|
437
|
+
/* Content entrance */
|
|
438
|
+
.content {
|
|
439
|
+
opacity: 0;
|
|
440
|
+
transform: translateY(20px);
|
|
441
|
+
animation: rise-in 500ms ease-out 800ms forwards;
|
|
442
|
+
}
|
|
443
|
+
h1 {
|
|
444
|
+
margin: 0 0 1rem 0;
|
|
445
|
+
font-size: 2rem;
|
|
446
|
+
line-height: 1.2;
|
|
447
|
+
}
|
|
448
|
+
p { margin: 0 0 1rem 0; color: var(--text-600); }
|
|
449
|
+
|
|
450
|
+
/* Error message box */
|
|
451
|
+
.error-box {
|
|
452
|
+
color: var(--text-900);
|
|
453
|
+
line-height: 1.6;
|
|
454
|
+
background: var(--error-bg);
|
|
455
|
+
padding: 1rem;
|
|
456
|
+
border-radius: 0.5rem;
|
|
457
|
+
border-left: 4px solid var(--error-border);
|
|
458
|
+
text-align: left;
|
|
459
|
+
word-wrap: break-word;
|
|
460
|
+
margin: 1rem 0 2rem 0;
|
|
461
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
462
|
+
white-space: pre-wrap;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/* Buttons */
|
|
466
|
+
.actions {
|
|
467
|
+
display: flex;
|
|
468
|
+
justify-content: center;
|
|
469
|
+
gap: 0.75rem;
|
|
470
|
+
flex-wrap: wrap;
|
|
471
|
+
}
|
|
472
|
+
.btn {
|
|
473
|
+
display: inline-block;
|
|
474
|
+
padding: 0.75rem 1.25rem;
|
|
475
|
+
border-radius: 9999px;
|
|
476
|
+
background: var(--ruby-500);
|
|
477
|
+
color: #fff;
|
|
478
|
+
border: none;
|
|
479
|
+
cursor: pointer;
|
|
480
|
+
box-shadow: 0 10px 20px rgba(0,0,0,0.15);
|
|
481
|
+
transition: transform 120ms ease, box-shadow 180ms ease, background-color 180ms ease;
|
|
482
|
+
font-weight: 600;
|
|
483
|
+
}
|
|
484
|
+
.btn:hover {
|
|
485
|
+
background: var(--ruby-600);
|
|
486
|
+
transform: scale(1.05);
|
|
487
|
+
box-shadow: 0 14px 28px rgba(0,0,0,0.2);
|
|
488
|
+
}
|
|
489
|
+
.btn:active { transform: scale(0.95); }
|
|
490
|
+
.btn.secondary {
|
|
491
|
+
background: #374151; /* neutral */
|
|
492
|
+
}
|
|
493
|
+
.btn.secondary:hover { background: #1f2937; }
|
|
494
|
+
.btn:focus-visible {
|
|
495
|
+
outline: 3px solid rgba(204,52,45,0.35);
|
|
496
|
+
outline-offset: 2px;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/* Animations */
|
|
500
|
+
@keyframes pop-in { to { opacity: 1; transform: scale(1); } }
|
|
501
|
+
@keyframes slide-in { to { opacity: 1; transform: translateY(0); } }
|
|
502
|
+
@keyframes draw-circle {
|
|
503
|
+
0% { opacity: 0; stroke-dashoffset: 339.292; }
|
|
504
|
+
20% { opacity: 1; }
|
|
505
|
+
100% { opacity: 1; stroke-dashoffset: 0; }
|
|
506
|
+
}
|
|
507
|
+
@keyframes draw-x {
|
|
508
|
+
0% { opacity: 0; stroke-dashoffset: 120; }
|
|
509
|
+
30% { opacity: 1; }
|
|
510
|
+
100% { opacity: 1; stroke-dashoffset: 0; }
|
|
511
|
+
}
|
|
512
|
+
@keyframes rise-in { to { opacity: 1; transform: translateY(0); } }
|
|
513
|
+
|
|
514
|
+
@media (prefers-reduced-motion: reduce) {
|
|
515
|
+
.card, .logo-card, .circle, .x1, .x2, .content { animation: none !important; }
|
|
516
|
+
.card, .logo-card { opacity: 1; transform: none; }
|
|
517
|
+
.content { opacity: 1; transform: none; }
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/* Responsive adjustments */
|
|
521
|
+
@media (max-width: 640px) {
|
|
522
|
+
.logo-card {
|
|
523
|
+
top: 0.75rem;
|
|
524
|
+
right: 0.75rem;
|
|
525
|
+
padding: 0.5rem;
|
|
526
|
+
gap: 0.375rem;
|
|
527
|
+
}
|
|
528
|
+
.logo { width: 28px; height: 28px; }
|
|
529
|
+
.logo-text { font-size: 0.75rem; }
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/* =========================
|
|
533
|
+
Dark Mode (automatic)
|
|
534
|
+
========================= */
|
|
535
|
+
@media (prefers-color-scheme: dark) {
|
|
536
|
+
:root {
|
|
537
|
+
--text-900: #F3F4F6; /* near-white */
|
|
538
|
+
--text-600: #D1D5DB; /* soft gray */
|
|
539
|
+
--card-bg: #151417; /* deep neutral */
|
|
540
|
+
--logo-border: rgba(255, 255, 255, 0.35);
|
|
541
|
+
--shadow-xl: 0 25px 50px -12px rgba(0,0,0,0.7);
|
|
542
|
+
|
|
543
|
+
/* Tweak error panel for dark mode */
|
|
544
|
+
--error-bg: #2a0c0c; /* subtle deep red panel */
|
|
545
|
+
--error-border: #EF4444; /* brighter red left bar */
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
body {
|
|
549
|
+
/* Brand-respecting darker background */
|
|
550
|
+
background:
|
|
551
|
+
radial-gradient(at 20% 30%, #560606 0%, transparent 50%),
|
|
552
|
+
radial-gradient(at 80% 70%, #2a2a2a 0%, transparent 40%),
|
|
553
|
+
radial-gradient(at 40% 80%, #6d1414 0%, transparent 50%),
|
|
554
|
+
radial-gradient(at 60% 20%, #3a2a2a 0%, transparent 45%),
|
|
555
|
+
radial-gradient(at 10% 70%, #0e0e0e 0%, transparent 50%),
|
|
556
|
+
radial-gradient(at 90% 30%, #1a1a1a 0%, transparent 45%),
|
|
557
|
+
linear-gradient(135deg, #7e1f1b 0%, #a62b26 50%, #b43b3b 100%);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.btn {
|
|
561
|
+
box-shadow: 0 10px 20px rgba(0,0,0,0.5);
|
|
562
|
+
}
|
|
563
|
+
.btn:hover {
|
|
564
|
+
box-shadow: 0 14px 28px rgba(0,0,0,0.6);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
</style>
|
|
568
|
+
</head>
|
|
569
|
+
<body>
|
|
570
|
+
<div class="logo-card">
|
|
571
|
+
<img
|
|
572
|
+
class="logo"
|
|
573
|
+
src="https://www.rubyllm-mcp.com/assets/images/rubyllm-mcp-logo.svg"
|
|
574
|
+
alt="RubyLLM MCP Logo"
|
|
575
|
+
decoding="async"
|
|
576
|
+
fetchpriority="high"
|
|
577
|
+
/>
|
|
578
|
+
<span class="logo-text">RubyLLM::MCP</span>
|
|
579
|
+
</div>
|
|
580
|
+
|
|
581
|
+
<main class="card" role="dialog" aria-labelledby="title" aria-describedby="desc">
|
|
582
|
+
<!-- Animated Error Icon -->
|
|
583
|
+
<div class="iconwrap" aria-hidden="true">
|
|
584
|
+
<svg class="erroricon" viewBox="0 0 120 120">
|
|
585
|
+
<circle class="circle" cx="60" cy="60" r="54"></circle>
|
|
586
|
+
<path class="x1" d="M 42 42 L 78 78"></path>
|
|
587
|
+
<path class="x2" d="M 78 42 L 42 78"></path>
|
|
588
|
+
</svg>
|
|
589
|
+
</div>
|
|
590
|
+
|
|
591
|
+
<!-- Message + details -->
|
|
592
|
+
<div class="content">
|
|
593
|
+
<h1 id="title">Authentication Failed</h1>
|
|
594
|
+
<p id="desc">Something went wrong while authenticating. See the details below:</p>
|
|
595
|
+
|
|
596
|
+
<div class="error-box">#{CGI.escapeHTML(error_message)}</div>
|
|
597
|
+
</div>
|
|
598
|
+
</main>
|
|
599
|
+
</body>
|
|
600
|
+
</html>
|
|
601
|
+
HTML
|
|
602
|
+
end
|
|
603
|
+
end
|
|
604
|
+
end
|
|
605
|
+
end
|
|
606
|
+
end
|
|
607
|
+
end
|