@cyanheads/mcp-ts-core 0.5.4 → 0.6.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.
- package/CLAUDE.md +39 -1
- package/README.md +1 -1
- package/changelog/0.1.x/0.1.0.md +78 -0
- package/changelog/0.1.x/0.1.1.md +28 -0
- package/changelog/0.1.x/0.1.10.md +32 -0
- package/changelog/0.1.x/0.1.11.md +51 -0
- package/changelog/0.1.x/0.1.12.md +21 -0
- package/changelog/0.1.x/0.1.13.md +16 -0
- package/changelog/0.1.x/0.1.14.md +20 -0
- package/changelog/0.1.x/0.1.15.md +24 -0
- package/changelog/0.1.x/0.1.16.md +17 -0
- package/changelog/0.1.x/0.1.17.md +14 -0
- package/changelog/0.1.x/0.1.18.md +18 -0
- package/changelog/0.1.x/0.1.19.md +19 -0
- package/changelog/0.1.x/0.1.2.md +25 -0
- package/changelog/0.1.x/0.1.20.md +21 -0
- package/changelog/0.1.x/0.1.21.md +17 -0
- package/changelog/0.1.x/0.1.22.md +28 -0
- package/changelog/0.1.x/0.1.23.md +23 -0
- package/changelog/0.1.x/0.1.24.md +17 -0
- package/changelog/0.1.x/0.1.25.md +16 -0
- package/changelog/0.1.x/0.1.26.md +22 -0
- package/changelog/0.1.x/0.1.27.md +30 -0
- package/changelog/0.1.x/0.1.28.md +16 -0
- package/changelog/0.1.x/0.1.29.md +19 -0
- package/changelog/0.1.x/0.1.3.md +22 -0
- package/changelog/0.1.x/0.1.4.md +17 -0
- package/changelog/0.1.x/0.1.5.md +25 -0
- package/changelog/0.1.x/0.1.6.md +26 -0
- package/changelog/0.1.x/0.1.7.md +29 -0
- package/changelog/0.1.x/0.1.8.md +33 -0
- package/changelog/0.1.x/0.1.9.md +19 -0
- package/changelog/0.2.x/0.2.0.md +32 -0
- package/changelog/0.2.x/0.2.1.md +12 -0
- package/changelog/0.2.x/0.2.10.md +38 -0
- package/changelog/0.2.x/0.2.11.md +29 -0
- package/changelog/0.2.x/0.2.12.md +31 -0
- package/changelog/0.2.x/0.2.2.md +19 -0
- package/changelog/0.2.x/0.2.3.md +15 -0
- package/changelog/0.2.x/0.2.4.md +24 -0
- package/changelog/0.2.x/0.2.5.md +27 -0
- package/changelog/0.2.x/0.2.6.md +23 -0
- package/changelog/0.2.x/0.2.7.md +23 -0
- package/changelog/0.2.x/0.2.8.md +12 -0
- package/changelog/0.2.x/0.2.9.md +25 -0
- package/changelog/0.3.x/0.3.0.md +45 -0
- package/changelog/0.3.x/0.3.1.md +16 -0
- package/changelog/0.3.x/0.3.2.md +24 -0
- package/changelog/0.3.x/0.3.3.md +31 -0
- package/changelog/0.3.x/0.3.4.md +31 -0
- package/changelog/0.3.x/0.3.5.md +32 -0
- package/changelog/0.3.x/0.3.6.md +48 -0
- package/changelog/0.3.x/0.3.7.md +23 -0
- package/changelog/0.3.x/0.3.8.md +21 -0
- package/changelog/0.4.x/0.4.0.md +38 -0
- package/changelog/0.4.x/0.4.1.md +31 -0
- package/changelog/0.5.x/0.5.0.md +29 -0
- package/changelog/0.5.x/0.5.1.md +18 -0
- package/changelog/0.5.x/0.5.2.md +38 -0
- package/changelog/0.5.x/0.5.3.md +26 -0
- package/changelog/0.5.x/0.5.4.md +29 -0
- package/changelog/0.6.x/0.6.0.md +39 -0
- package/changelog/unreleased.md +40 -0
- package/dist/cli/init.js +1 -0
- package/dist/cli/init.js.map +1 -1
- package/dist/core/app.d.ts +13 -3
- package/dist/core/app.d.ts.map +1 -1
- package/dist/core/app.js +20 -13
- package/dist/core/app.js.map +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/serverManifest.d.ts +237 -0
- package/dist/core/serverManifest.d.ts.map +1 -0
- package/dist/core/serverManifest.js +310 -0
- package/dist/core/serverManifest.js.map +1 -0
- package/dist/core/worker.d.ts.map +1 -1
- package/dist/core/worker.js +2 -2
- package/dist/core/worker.js.map +1 -1
- package/dist/linter/rules/landing-rules.d.ts +15 -0
- package/dist/linter/rules/landing-rules.d.ts.map +1 -0
- package/dist/linter/rules/landing-rules.js +125 -0
- package/dist/linter/rules/landing-rules.js.map +1 -0
- package/dist/linter/types.d.ts +5 -2
- package/dist/linter/types.d.ts.map +1 -1
- package/dist/linter/validate.d.ts.map +1 -1
- package/dist/linter/validate.js +5 -0
- package/dist/linter/validate.js.map +1 -1
- package/dist/mcp-server/transports/http/httpTransport.d.ts +4 -3
- package/dist/mcp-server/transports/http/httpTransport.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/httpTransport.js +47 -26
- package/dist/mcp-server/transports/http/httpTransport.js.map +1 -1
- package/dist/mcp-server/transports/http/httpTypes.d.ts +0 -12
- package/dist/mcp-server/transports/http/httpTypes.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/landing-page.d.ts +48 -0
- package/dist/mcp-server/transports/http/landing-page.d.ts.map +1 -0
- package/dist/mcp-server/transports/http/landing-page.js +912 -0
- package/dist/mcp-server/transports/http/landing-page.js.map +1 -0
- package/dist/mcp-server/transports/http/serverCard.d.ts +67 -0
- package/dist/mcp-server/transports/http/serverCard.d.ts.map +1 -0
- package/dist/mcp-server/transports/http/serverCard.js +91 -0
- package/dist/mcp-server/transports/http/serverCard.js.map +1 -0
- package/dist/mcp-server/transports/manager.d.ts +3 -3
- package/dist/mcp-server/transports/manager.d.ts.map +1 -1
- package/dist/mcp-server/transports/manager.js +4 -4
- package/dist/mcp-server/transports/manager.js.map +1 -1
- package/dist/utils/formatting/html.d.ts +76 -0
- package/dist/utils/formatting/html.d.ts.map +1 -0
- package/dist/utils/formatting/html.js +111 -0
- package/dist/utils/formatting/html.js.map +1 -0
- package/dist/utils/formatting/index.d.ts +1 -0
- package/dist/utils/formatting/index.d.ts.map +1 -1
- package/dist/utils/formatting/index.js +1 -0
- package/dist/utils/formatting/index.js.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +5 -1
- package/scripts/build-changelog.ts +222 -0
- package/scripts/devcheck.ts +16 -1
- package/scripts/tree.ts +3 -0
- package/skills/add-app-tool/SKILL.md +2 -4
- package/skills/add-prompt/SKILL.md +2 -4
- package/skills/add-resource/SKILL.md +2 -4
- package/skills/add-service/SKILL.md +2 -4
- package/skills/add-tool/SKILL.md +6 -5
- package/skills/api-context/SKILL.md +2 -2
- package/skills/api-services/SKILL.md +1 -1
- package/skills/api-services/references/graph.md +1 -1
- package/skills/api-utils/SKILL.md +1 -1
- package/skills/api-utils/references/parsing.md +1 -1
- package/skills/api-utils/references/security.md +1 -1
- package/skills/design-mcp-server/SKILL.md +2 -2
- package/skills/maintenance/SKILL.md +12 -11
- package/skills/polish-docs-meta/SKILL.md +24 -9
- package/skills/release/SKILL.md +21 -7
- package/skills/setup/SKILL.md +4 -8
- package/templates/AGENTS.md +23 -1
- package/templates/CLAUDE.md +23 -1
- package/templates/changelog/unreleased.md +40 -0
- package/templates/package.json +3 -0
|
@@ -0,0 +1,912 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview HTML landing page served at `GET /`. Self-contained,
|
|
3
|
+
* zero-dependency renderer producing a branded, a11y-conscious,
|
|
4
|
+
* `prefers-color-scheme`-aware page from the shared `ServerManifest`.
|
|
5
|
+
*
|
|
6
|
+
* The page is the human-facing sibling of `/mcp` (bespoke JSON) and
|
|
7
|
+
* `/.well-known/mcp.json` (SEP-1649 Server Card). Framework owns the design
|
|
8
|
+
* system; servers supply content through `LandingConfig`.
|
|
9
|
+
*
|
|
10
|
+
* ## Surfaces
|
|
11
|
+
*
|
|
12
|
+
* - Hero — name, clickable version badge, pre-release pill, tagline, logo,
|
|
13
|
+
* auth-status banner, copy-to-clipboard connect snippets
|
|
14
|
+
* - Tools section — counts in header; auto-grouped by shared prefix; per-card
|
|
15
|
+
* annotations, invocation snippet, view-source link, schema preview
|
|
16
|
+
* - Resources section — URI template, mime type, description, view-source link
|
|
17
|
+
* - Prompts section — args list, view-source link
|
|
18
|
+
* - Extensions section — rendered when SEP-2133 extensions are present
|
|
19
|
+
* - Footer — configured links + auto-derived GitHub cluster + npm/registry +
|
|
20
|
+
* attribution
|
|
21
|
+
*
|
|
22
|
+
* @module src/mcp-server/transports/http/landing-page
|
|
23
|
+
*/
|
|
24
|
+
import { escapeHtml, html, unsafeRaw } from '../../../utils/formatting/html.js';
|
|
25
|
+
import { logger } from '../../../utils/internal/logger.js';
|
|
26
|
+
import { requestContextService } from '../../../utils/internal/requestContext.js';
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Tokens — inlined once per page
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
/** Single inlined `<style>` block. No external CSS, no fonts. */
|
|
31
|
+
function renderTokens(accent) {
|
|
32
|
+
const safeAccent = escapeHtml(accent);
|
|
33
|
+
const css = `
|
|
34
|
+
:root {
|
|
35
|
+
--space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px;
|
|
36
|
+
--space-5: 20px; --space-6: 24px; --space-8: 32px; --space-10: 40px;
|
|
37
|
+
--space-12: 48px; --space-16: 64px;
|
|
38
|
+
|
|
39
|
+
--text-xs: 0.75rem; --text-sm: 0.875rem; --text-base: 1rem;
|
|
40
|
+
--text-lg: 1.125rem; --text-xl: 1.25rem; --text-2xl: 1.5rem;
|
|
41
|
+
--text-3xl: 1.875rem; --text-4xl: 2.25rem;
|
|
42
|
+
|
|
43
|
+
--radius-sm: 6px; --radius-md: 8px; --radius-lg: 12px; --radius-pill: 999px;
|
|
44
|
+
|
|
45
|
+
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
46
|
+
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
|
|
47
|
+
|
|
48
|
+
--duration-fast: 120ms; --duration-base: 200ms;
|
|
49
|
+
--ease-out: cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
50
|
+
|
|
51
|
+
--accent: ${safeAccent};
|
|
52
|
+
--accent-fg: color-mix(in oklab, ${safeAccent}, white 85%);
|
|
53
|
+
--accent-hover: color-mix(in oklab, ${safeAccent}, black 12%);
|
|
54
|
+
--accent-soft: color-mix(in oklab, ${safeAccent}, transparent 86%);
|
|
55
|
+
|
|
56
|
+
--bg: #ffffff;
|
|
57
|
+
--bg-subtle: #f6f8fa;
|
|
58
|
+
--bg-elevated: #ffffff;
|
|
59
|
+
--fg: #1f2328;
|
|
60
|
+
--fg-muted: #656d76;
|
|
61
|
+
--fg-subtle: #8c959f;
|
|
62
|
+
--border: #d0d7de;
|
|
63
|
+
--border-subtle: #eaeef2;
|
|
64
|
+
--shadow-sm: 0 1px 2px rgb(0 0 0 / 0.04);
|
|
65
|
+
--shadow-md: 0 4px 16px -4px rgb(0 0 0 / 0.08), 0 1px 2px rgb(0 0 0 / 0.04);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@media (prefers-color-scheme: dark) {
|
|
69
|
+
:root {
|
|
70
|
+
--bg: #0d1117;
|
|
71
|
+
--bg-subtle: #161b22;
|
|
72
|
+
--bg-elevated: #161b22;
|
|
73
|
+
--fg: #e6edf3;
|
|
74
|
+
--fg-muted: #8d96a0;
|
|
75
|
+
--fg-subtle: #6e7681;
|
|
76
|
+
--border: #30363d;
|
|
77
|
+
--border-subtle: #21262d;
|
|
78
|
+
--shadow-sm: 0 1px 2px rgb(0 0 0 / 0.3);
|
|
79
|
+
--shadow-md: 0 4px 16px -4px rgb(0 0 0 / 0.4), 0 1px 2px rgb(0 0 0 / 0.2);
|
|
80
|
+
--accent-fg: color-mix(in oklab, ${safeAccent}, white 10%);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
85
|
+
|
|
86
|
+
html { color-scheme: light dark; }
|
|
87
|
+
|
|
88
|
+
body {
|
|
89
|
+
margin: 0;
|
|
90
|
+
font-family: var(--font-sans);
|
|
91
|
+
font-size: var(--text-base);
|
|
92
|
+
line-height: 1.5;
|
|
93
|
+
color: var(--fg);
|
|
94
|
+
background: var(--bg);
|
|
95
|
+
-webkit-font-smoothing: antialiased;
|
|
96
|
+
-moz-osx-font-smoothing: grayscale;
|
|
97
|
+
text-rendering: optimizeLegibility;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
::selection { background: var(--accent-soft); color: var(--fg); }
|
|
101
|
+
|
|
102
|
+
main { max-width: 960px; margin: 0 auto; padding: var(--space-8) var(--space-6) var(--space-16); }
|
|
103
|
+
|
|
104
|
+
a {
|
|
105
|
+
color: var(--accent);
|
|
106
|
+
text-decoration: none;
|
|
107
|
+
transition: color var(--duration-fast) var(--ease-out);
|
|
108
|
+
}
|
|
109
|
+
a:hover { color: var(--accent-hover); text-decoration: underline; }
|
|
110
|
+
a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }
|
|
111
|
+
|
|
112
|
+
code, pre {
|
|
113
|
+
font-family: var(--font-mono);
|
|
114
|
+
font-size: 0.85em;
|
|
115
|
+
}
|
|
116
|
+
code { background: var(--bg-subtle); padding: 0.15em 0.35em; border-radius: var(--radius-sm); border: 1px solid var(--border-subtle); }
|
|
117
|
+
pre {
|
|
118
|
+
background: var(--bg-subtle);
|
|
119
|
+
border: 1px solid var(--border-subtle);
|
|
120
|
+
border-radius: var(--radius-md);
|
|
121
|
+
padding: var(--space-4);
|
|
122
|
+
margin: 0;
|
|
123
|
+
overflow-x: auto;
|
|
124
|
+
line-height: 1.5;
|
|
125
|
+
white-space: pre;
|
|
126
|
+
}
|
|
127
|
+
pre code { background: transparent; padding: 0; border: 0; }
|
|
128
|
+
|
|
129
|
+
.hero { padding: var(--space-10) 0 var(--space-8); border-bottom: 1px solid var(--border-subtle); }
|
|
130
|
+
.hero-top { display: flex; align-items: flex-start; gap: var(--space-4); margin-bottom: var(--space-4); }
|
|
131
|
+
.hero-logo { width: 56px; height: 56px; border-radius: var(--radius-md); object-fit: contain; background: var(--bg-subtle); border: 1px solid var(--border-subtle); flex-shrink: 0; }
|
|
132
|
+
.hero-identity { flex: 1; min-width: 0; }
|
|
133
|
+
.hero-heading { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-3); margin: 0; font-size: var(--text-3xl); font-weight: 700; letter-spacing: -0.02em; color: var(--fg); }
|
|
134
|
+
.hero-tagline { margin: var(--space-3) 0 0; color: var(--fg-muted); font-size: var(--text-lg); max-width: 60ch; }
|
|
135
|
+
|
|
136
|
+
.badge {
|
|
137
|
+
display: inline-flex; align-items: center; gap: var(--space-1);
|
|
138
|
+
padding: 2px var(--space-2);
|
|
139
|
+
border-radius: var(--radius-pill);
|
|
140
|
+
font-size: var(--text-xs);
|
|
141
|
+
font-weight: 500;
|
|
142
|
+
line-height: 1.4;
|
|
143
|
+
border: 1px solid var(--border);
|
|
144
|
+
color: var(--fg-muted);
|
|
145
|
+
background: var(--bg-subtle);
|
|
146
|
+
text-decoration: none;
|
|
147
|
+
white-space: nowrap;
|
|
148
|
+
}
|
|
149
|
+
.badge-version { color: var(--accent); border-color: var(--accent-soft); background: var(--accent-soft); font-weight: 600; }
|
|
150
|
+
.badge-version:hover { background: color-mix(in oklab, var(--accent), transparent 75%); text-decoration: none; }
|
|
151
|
+
.badge-pre { background: color-mix(in oklab, #f59e0b, transparent 85%); border-color: color-mix(in oklab, #f59e0b, transparent 60%); color: #b45309; }
|
|
152
|
+
@media (prefers-color-scheme: dark) { .badge-pre { color: #fbbf24; } }
|
|
153
|
+
|
|
154
|
+
.hero-badges { margin-top: var(--space-4); display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }
|
|
155
|
+
|
|
156
|
+
.badge-shield {
|
|
157
|
+
display: inline-flex;
|
|
158
|
+
align-items: stretch;
|
|
159
|
+
border-radius: var(--radius-sm);
|
|
160
|
+
overflow: hidden;
|
|
161
|
+
font-size: 0.7rem;
|
|
162
|
+
font-weight: 600;
|
|
163
|
+
line-height: 1.4;
|
|
164
|
+
letter-spacing: 0.01em;
|
|
165
|
+
text-decoration: none;
|
|
166
|
+
font-family: var(--font-sans);
|
|
167
|
+
box-shadow: 0 0 0 1px rgb(0 0 0 / 0.04), 0 1px 1px rgb(0 0 0 / 0.04);
|
|
168
|
+
transition: transform var(--duration-fast) var(--ease-out), box-shadow var(--duration-fast) var(--ease-out);
|
|
169
|
+
}
|
|
170
|
+
.badge-shield:hover { text-decoration: none; transform: translateY(-1px); box-shadow: 0 0 0 1px rgb(0 0 0 / 0.08), 0 2px 4px rgb(0 0 0 / 0.08); }
|
|
171
|
+
.badge-shield-label, .badge-shield-value { padding: 3px var(--space-2); white-space: nowrap; }
|
|
172
|
+
.badge-shield-label { background: #555; color: #fff; }
|
|
173
|
+
.badge-shield-value { background: #2259c9; color: #fff; }
|
|
174
|
+
@media (prefers-color-scheme: dark) {
|
|
175
|
+
.badge-shield-label { background: #3a3a3a; }
|
|
176
|
+
.badge-shield-value { background: #3b6fd4; }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.auth-banner {
|
|
180
|
+
margin: var(--space-5) 0 0;
|
|
181
|
+
padding: var(--space-3) var(--space-4);
|
|
182
|
+
border-radius: var(--radius-md);
|
|
183
|
+
border: 1px solid var(--border-subtle);
|
|
184
|
+
background: var(--bg-subtle);
|
|
185
|
+
color: var(--fg-muted);
|
|
186
|
+
font-size: var(--text-sm);
|
|
187
|
+
display: flex; align-items: center; gap: var(--space-2);
|
|
188
|
+
}
|
|
189
|
+
.auth-banner-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
190
|
+
.auth-banner-public .auth-banner-dot { background: #22c55e; }
|
|
191
|
+
.auth-banner-gated .auth-banner-dot { background: var(--accent); }
|
|
192
|
+
|
|
193
|
+
section { padding: var(--space-10) 0 0; }
|
|
194
|
+
.section-heading { display: flex; align-items: baseline; gap: var(--space-3); margin: 0 0 var(--space-6); }
|
|
195
|
+
.section-heading h2 { margin: 0; font-size: var(--text-2xl); font-weight: 600; letter-spacing: -0.01em; }
|
|
196
|
+
.section-count { color: var(--fg-subtle); font-size: var(--text-sm); font-variant-numeric: tabular-nums; font-weight: 500; }
|
|
197
|
+
|
|
198
|
+
.group-heading { margin: var(--space-6) 0 var(--space-3); color: var(--fg-muted); font-size: var(--text-xs); font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; }
|
|
199
|
+
.group-heading:first-child { margin-top: 0; }
|
|
200
|
+
|
|
201
|
+
.card {
|
|
202
|
+
border: 1px solid var(--border-subtle);
|
|
203
|
+
border-radius: var(--radius-lg);
|
|
204
|
+
padding: var(--space-5);
|
|
205
|
+
margin-bottom: var(--space-3);
|
|
206
|
+
background: var(--bg-elevated);
|
|
207
|
+
transition: border-color var(--duration-fast) var(--ease-out), box-shadow var(--duration-fast) var(--ease-out);
|
|
208
|
+
}
|
|
209
|
+
.card:hover { border-color: var(--border); box-shadow: var(--shadow-sm); }
|
|
210
|
+
.card-head { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; margin-bottom: var(--space-2); }
|
|
211
|
+
.card-title { margin: 0; font-size: var(--text-base); font-weight: 600; font-family: var(--font-mono); color: var(--fg); }
|
|
212
|
+
.card-title a { color: inherit; }
|
|
213
|
+
.card-desc { margin: 0; color: var(--fg-muted); font-size: var(--text-sm); }
|
|
214
|
+
.card-meta { margin-top: var(--space-3); display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); font-size: var(--text-xs); color: var(--fg-muted); align-items: center; }
|
|
215
|
+
.card-meta-label { color: var(--fg-subtle); }
|
|
216
|
+
.card-meta code { font-size: 0.9em; }
|
|
217
|
+
|
|
218
|
+
.pill-row { display: inline-flex; flex-wrap: wrap; gap: var(--space-1); }
|
|
219
|
+
.pill {
|
|
220
|
+
display: inline-flex; align-items: center;
|
|
221
|
+
padding: 1px 8px;
|
|
222
|
+
border-radius: var(--radius-pill);
|
|
223
|
+
font-size: 0.7rem;
|
|
224
|
+
font-weight: 500;
|
|
225
|
+
line-height: 1.5;
|
|
226
|
+
border: 1px solid var(--border-subtle);
|
|
227
|
+
color: var(--fg-muted);
|
|
228
|
+
background: var(--bg-subtle);
|
|
229
|
+
}
|
|
230
|
+
.pill-readonly { color: #16a34a; border-color: color-mix(in oklab, #16a34a, transparent 70%); background: color-mix(in oklab, #16a34a, transparent 90%); }
|
|
231
|
+
.pill-destructive { color: #dc2626; border-color: color-mix(in oklab, #dc2626, transparent 70%); background: color-mix(in oklab, #dc2626, transparent 90%); }
|
|
232
|
+
.pill-openworld { color: #2563eb; border-color: color-mix(in oklab, #2563eb, transparent 70%); background: color-mix(in oklab, #2563eb, transparent 90%); }
|
|
233
|
+
.pill-task { color: var(--accent); border-color: var(--accent-soft); background: var(--accent-soft); }
|
|
234
|
+
.pill-app { color: #9333ea; border-color: color-mix(in oklab, #9333ea, transparent 70%); background: color-mix(in oklab, #9333ea, transparent 90%); }
|
|
235
|
+
.pill-auth { color: var(--fg-muted); font-family: var(--font-mono); font-size: 0.65rem; }
|
|
236
|
+
|
|
237
|
+
.snippet { position: relative; margin-top: var(--space-3); }
|
|
238
|
+
.snippet pre { padding-right: var(--space-12); font-size: 0.8rem; }
|
|
239
|
+
.snippet-copy {
|
|
240
|
+
position: absolute; top: var(--space-2); right: var(--space-2);
|
|
241
|
+
font-family: var(--font-sans);
|
|
242
|
+
font-size: var(--text-xs);
|
|
243
|
+
padding: 2px var(--space-2);
|
|
244
|
+
border-radius: var(--radius-sm);
|
|
245
|
+
border: 1px solid var(--border);
|
|
246
|
+
background: var(--bg);
|
|
247
|
+
color: var(--fg-muted);
|
|
248
|
+
cursor: pointer;
|
|
249
|
+
transition: color var(--duration-fast), border-color var(--duration-fast);
|
|
250
|
+
}
|
|
251
|
+
.snippet-copy:hover { color: var(--accent); border-color: var(--accent); }
|
|
252
|
+
.snippet-copy:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
253
|
+
.snippet-copy[data-copied="true"] { color: #16a34a; border-color: #16a34a; }
|
|
254
|
+
|
|
255
|
+
details { margin-top: var(--space-3); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); }
|
|
256
|
+
details > summary {
|
|
257
|
+
cursor: pointer;
|
|
258
|
+
padding: var(--space-2) var(--space-3);
|
|
259
|
+
font-size: var(--text-xs);
|
|
260
|
+
color: var(--fg-muted);
|
|
261
|
+
list-style: none;
|
|
262
|
+
user-select: none;
|
|
263
|
+
transition: background var(--duration-fast);
|
|
264
|
+
}
|
|
265
|
+
details > summary::-webkit-details-marker { display: none; }
|
|
266
|
+
details > summary:hover { background: var(--bg-subtle); color: var(--fg); }
|
|
267
|
+
details > summary::before { content: "▸ "; margin-right: 4px; transition: transform var(--duration-fast); display: inline-block; color: var(--fg-subtle); }
|
|
268
|
+
details[open] > summary::before { transform: rotate(90deg); }
|
|
269
|
+
details[open] > summary { border-bottom: 1px solid var(--border-subtle); }
|
|
270
|
+
details > pre { border: 0; border-radius: 0 0 var(--radius-md) var(--radius-md); margin: 0; }
|
|
271
|
+
|
|
272
|
+
.connect-tabs { display: flex; gap: var(--space-1); margin-top: var(--space-4); border-bottom: 1px solid var(--border-subtle); }
|
|
273
|
+
.connect-tab-input { position: absolute; opacity: 0; pointer-events: none; }
|
|
274
|
+
.connect-tab-label {
|
|
275
|
+
padding: var(--space-2) var(--space-4);
|
|
276
|
+
font-size: var(--text-sm);
|
|
277
|
+
color: var(--fg-muted);
|
|
278
|
+
cursor: pointer;
|
|
279
|
+
border-bottom: 2px solid transparent;
|
|
280
|
+
margin-bottom: -1px;
|
|
281
|
+
transition: color var(--duration-fast), border-color var(--duration-fast);
|
|
282
|
+
}
|
|
283
|
+
.connect-tab-label:hover { color: var(--fg); }
|
|
284
|
+
.connect-tab-input:focus-visible + .connect-tab-label { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-sm); }
|
|
285
|
+
.connect-panels { position: relative; margin-top: var(--space-3); }
|
|
286
|
+
.connect-panel { display: none; }
|
|
287
|
+
.connect-tab-input:checked + .connect-tab-label { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
|
|
288
|
+
${[0, 1, 2]
|
|
289
|
+
.map((i) => `.connect-tab-input:nth-of-type(${i + 1}):checked ~ .connect-panels .connect-panel:nth-child(${i + 1}) { display: block; }`)
|
|
290
|
+
.join('\n')}
|
|
291
|
+
|
|
292
|
+
footer {
|
|
293
|
+
margin-top: var(--space-16);
|
|
294
|
+
padding: var(--space-8) 0 var(--space-6);
|
|
295
|
+
border-top: 1px solid var(--border-subtle);
|
|
296
|
+
font-size: var(--text-sm);
|
|
297
|
+
color: var(--fg-muted);
|
|
298
|
+
}
|
|
299
|
+
.footer-links { display: flex; flex-wrap: wrap; gap: var(--space-4) var(--space-6); margin-bottom: var(--space-4); }
|
|
300
|
+
.footer-group { display: flex; flex-direction: column; gap: var(--space-2); min-width: 140px; }
|
|
301
|
+
.footer-group-label { color: var(--fg-subtle); font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600; }
|
|
302
|
+
.footer-attrib { font-size: var(--text-xs); color: var(--fg-subtle); }
|
|
303
|
+
.footer-attrib a { color: inherit; text-decoration: underline; text-decoration-color: var(--border); }
|
|
304
|
+
.footer-attrib a:hover { color: var(--accent); text-decoration-color: var(--accent); }
|
|
305
|
+
|
|
306
|
+
.source-link { font-size: var(--text-xs); color: var(--fg-muted); margin-left: auto; }
|
|
307
|
+
.source-link:hover { color: var(--accent); }
|
|
308
|
+
|
|
309
|
+
.args-list { list-style: none; padding: 0; margin: var(--space-3) 0 0; display: flex; flex-direction: column; gap: var(--space-1); font-size: var(--text-sm); }
|
|
310
|
+
.args-list code { font-size: 0.85em; }
|
|
311
|
+
.args-required { color: var(--accent); font-size: 0.7rem; font-weight: 600; margin-left: var(--space-1); }
|
|
312
|
+
|
|
313
|
+
.ext-card { background: var(--bg-subtle); border-color: var(--border-subtle); }
|
|
314
|
+
.ext-key { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--fg); }
|
|
315
|
+
.ext-preview { margin-top: var(--space-2); font-size: var(--text-xs); color: var(--fg-muted); }
|
|
316
|
+
|
|
317
|
+
.empty-state { padding: var(--space-8); text-align: center; color: var(--fg-muted); border: 1px dashed var(--border); border-radius: var(--radius-md); }
|
|
318
|
+
|
|
319
|
+
@media (max-width: 640px) {
|
|
320
|
+
main { padding: var(--space-5) var(--space-4) var(--space-12); }
|
|
321
|
+
.hero-heading { font-size: var(--text-2xl); }
|
|
322
|
+
.hero-top { flex-direction: column; gap: var(--space-3); }
|
|
323
|
+
.hero-logo { width: 48px; height: 48px; }
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
@media (prefers-reduced-motion: reduce) {
|
|
327
|
+
*, *::before, *::after {
|
|
328
|
+
transition-duration: 0.01ms !important;
|
|
329
|
+
animation-duration: 0.01ms !important;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
`;
|
|
333
|
+
return unsafeRaw(`<style>${css}</style>`);
|
|
334
|
+
}
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
// Copy-to-clipboard — single inlined script, < 1KB
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
function renderCopyScript() {
|
|
339
|
+
// Works without JS too — the button is inert but the pre/code is still selectable.
|
|
340
|
+
// `data-copy-target` holds a CSS selector or inline text content.
|
|
341
|
+
const js = `
|
|
342
|
+
document.addEventListener('click', function(e) {
|
|
343
|
+
var btn = e.target.closest('[data-copy]');
|
|
344
|
+
if (!btn) return;
|
|
345
|
+
var selector = btn.getAttribute('data-copy-target');
|
|
346
|
+
var text = '';
|
|
347
|
+
if (selector) {
|
|
348
|
+
var node = document.querySelector(selector);
|
|
349
|
+
if (node) text = node.textContent || '';
|
|
350
|
+
} else {
|
|
351
|
+
text = btn.getAttribute('data-copy') || '';
|
|
352
|
+
}
|
|
353
|
+
if (!text || !navigator.clipboard) return;
|
|
354
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
355
|
+
var prev = btn.textContent;
|
|
356
|
+
btn.setAttribute('data-copied', 'true');
|
|
357
|
+
btn.textContent = 'Copied';
|
|
358
|
+
setTimeout(function() {
|
|
359
|
+
btn.removeAttribute('data-copied');
|
|
360
|
+
btn.textContent = prev;
|
|
361
|
+
}, 1500);
|
|
362
|
+
});
|
|
363
|
+
});`;
|
|
364
|
+
return unsafeRaw(`<script>${js}</script>`);
|
|
365
|
+
}
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
// Primitives
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
function renderBadge(label, variant, href) {
|
|
370
|
+
const cls = variant === 'version' ? 'badge badge-version' : variant === 'pre' ? 'badge badge-pre' : 'badge';
|
|
371
|
+
if (href) {
|
|
372
|
+
return html `<a class="${cls}" href="${href}">${label}</a>`;
|
|
373
|
+
}
|
|
374
|
+
return html `<span class="${cls}">${label}</span>`;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* shields.io-style bi-part badge: grey "Built on" label + accent framework name.
|
|
378
|
+
* Links to the framework's npm page for discoverability. Lives in the hero
|
|
379
|
+
* badge strip when `landing.attribution` is enabled.
|
|
380
|
+
*/
|
|
381
|
+
function renderFrameworkBadge(framework) {
|
|
382
|
+
const npmUrl = `https://www.npmjs.com/package/${encodeURIComponent(framework.name)}`;
|
|
383
|
+
return html `<a class="badge-shield" href="${npmUrl}" rel="noopener" aria-label="Built on ${framework.name} v${framework.version}"><span class="badge-shield-label">Built on</span><span class="badge-shield-value">${framework.name} v${framework.version}</span></a>`;
|
|
384
|
+
}
|
|
385
|
+
function renderPill(text, variant) {
|
|
386
|
+
return html `<span class="pill pill-${variant}">${text}</span>`;
|
|
387
|
+
}
|
|
388
|
+
function renderAuthBanner(auth) {
|
|
389
|
+
if (auth.mode === 'none') {
|
|
390
|
+
return html `<div class="auth-banner auth-banner-public" role="status"><span class="auth-banner-dot" aria-hidden="true"></span><span>Public access — no authentication required.</span></div>`;
|
|
391
|
+
}
|
|
392
|
+
if (auth.mode === 'jwt') {
|
|
393
|
+
return html `<div class="auth-banner auth-banner-gated" role="status"><span class="auth-banner-dot" aria-hidden="true"></span><span>Requires a bearer token.</span></div>`;
|
|
394
|
+
}
|
|
395
|
+
// oauth
|
|
396
|
+
const issuer = auth.oauthIssuer ? html ` <a href="${auth.oauthIssuer}">Sign in ↗</a>` : html ``;
|
|
397
|
+
return html `<div class="auth-banner auth-banner-gated" role="status"><span class="auth-banner-dot" aria-hidden="true"></span><span>Requires OAuth.${issuer}</span></div>`;
|
|
398
|
+
}
|
|
399
|
+
function renderSectionHeading(id, label, count) {
|
|
400
|
+
return html `
|
|
401
|
+
<div class="section-heading">
|
|
402
|
+
<h2 id="${id}">${label}</h2>
|
|
403
|
+
<span class="section-count" aria-label="${String(count)} ${label.toLowerCase()}">(${count})</span>
|
|
404
|
+
</div>
|
|
405
|
+
`;
|
|
406
|
+
}
|
|
407
|
+
function renderSnippet(id, text, variant = 'code') {
|
|
408
|
+
const targetId = `snippet-${id}`;
|
|
409
|
+
return html `
|
|
410
|
+
<div class="snippet">
|
|
411
|
+
<pre id="${targetId}" class="snippet-${variant}"><code>${text}</code></pre>
|
|
412
|
+
<button type="button" class="snippet-copy" data-copy data-copy-target="#${targetId}" aria-label="Copy">Copy</button>
|
|
413
|
+
</div>
|
|
414
|
+
`;
|
|
415
|
+
}
|
|
416
|
+
// ---------------------------------------------------------------------------
|
|
417
|
+
// Hero
|
|
418
|
+
// ---------------------------------------------------------------------------
|
|
419
|
+
function renderHero(manifest, baseUrl) {
|
|
420
|
+
const { server, landing } = manifest;
|
|
421
|
+
const releaseUrl = landing.repoRoot
|
|
422
|
+
? `${landing.repoRoot.url}/releases/tag/v${server.version}`
|
|
423
|
+
: undefined;
|
|
424
|
+
const versionBadge = renderBadge(`v${server.version}`, 'version', releaseUrl);
|
|
425
|
+
const preReleaseBadge = landing.preRelease.isPreRelease
|
|
426
|
+
? renderBadge(landing.preRelease.label ?? 'pre-release', 'pre')
|
|
427
|
+
: html ``;
|
|
428
|
+
const tagline = landing.tagline ?? server.description ?? '';
|
|
429
|
+
const logo = landing.logo
|
|
430
|
+
? html `<img class="hero-logo" src="${landing.logo}" alt="" aria-hidden="true" />`
|
|
431
|
+
: html ``;
|
|
432
|
+
const frameworkBadge = landing.attribution
|
|
433
|
+
? html `<div class="hero-badges">${renderFrameworkBadge(manifest.framework)}</div>`
|
|
434
|
+
: html ``;
|
|
435
|
+
return html `
|
|
436
|
+
<header class="hero">
|
|
437
|
+
<div class="hero-top">
|
|
438
|
+
${logo}
|
|
439
|
+
<div class="hero-identity">
|
|
440
|
+
<h1 class="hero-heading">
|
|
441
|
+
<span>${server.name}</span>
|
|
442
|
+
${versionBadge}
|
|
443
|
+
${preReleaseBadge}
|
|
444
|
+
</h1>
|
|
445
|
+
${tagline ? html `<p class="hero-tagline">${tagline}</p>` : html ``}
|
|
446
|
+
${frameworkBadge}
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
${renderAuthBanner(manifest.auth)}
|
|
450
|
+
${renderConnectSnippets(manifest, baseUrl)}
|
|
451
|
+
</header>
|
|
452
|
+
`;
|
|
453
|
+
}
|
|
454
|
+
function renderConnectSnippets(manifest, baseUrl) {
|
|
455
|
+
const endpoint = `${baseUrl.replace(/\/$/, '')}${manifest.transport.endpointPath}`;
|
|
456
|
+
const claudeDesktopConfig = JSON.stringify({
|
|
457
|
+
mcpServers: {
|
|
458
|
+
[manifest.server.name]: {
|
|
459
|
+
type: 'http',
|
|
460
|
+
url: endpoint,
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
}, null, 2);
|
|
464
|
+
const mcpRemoteConfig = JSON.stringify({
|
|
465
|
+
mcpServers: {
|
|
466
|
+
[manifest.server.name]: {
|
|
467
|
+
command: 'npx',
|
|
468
|
+
args: ['-y', 'mcp-remote', endpoint],
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
}, null, 2);
|
|
472
|
+
const curl = [
|
|
473
|
+
`curl -X POST ${endpoint} \\`,
|
|
474
|
+
` -H "Content-Type: application/json" \\`,
|
|
475
|
+
` -H "MCP-Protocol-Version: ${manifest.protocol.latestVersion}" \\`,
|
|
476
|
+
` -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"${manifest.protocol.latestVersion}","capabilities":{},"clientInfo":{"name":"curl","version":"1.0.0"}}}'`,
|
|
477
|
+
].join('\n');
|
|
478
|
+
// Radio-hack tabs — no JS required for tab switching.
|
|
479
|
+
return html `
|
|
480
|
+
<div class="connect-tabs" role="tablist" aria-label="Connection snippets">
|
|
481
|
+
<input type="radio" class="connect-tab-input" name="connect" id="connect-tab-http" checked />
|
|
482
|
+
<label for="connect-tab-http" class="connect-tab-label" role="tab">HTTP client</label>
|
|
483
|
+
<input type="radio" class="connect-tab-input" name="connect" id="connect-tab-remote" />
|
|
484
|
+
<label for="connect-tab-remote" class="connect-tab-label" role="tab">mcp-remote (stdio)</label>
|
|
485
|
+
<input type="radio" class="connect-tab-input" name="connect" id="connect-tab-curl" />
|
|
486
|
+
<label for="connect-tab-curl" class="connect-tab-label" role="tab">curl</label>
|
|
487
|
+
<div class="connect-panels">
|
|
488
|
+
<div class="connect-panel" role="tabpanel">${renderSnippet('http', claudeDesktopConfig)}</div>
|
|
489
|
+
<div class="connect-panel" role="tabpanel">${renderSnippet('remote', mcpRemoteConfig)}</div>
|
|
490
|
+
<div class="connect-panel" role="tabpanel">${renderSnippet('curl', curl)}</div>
|
|
491
|
+
</div>
|
|
492
|
+
</div>
|
|
493
|
+
`;
|
|
494
|
+
}
|
|
495
|
+
// ---------------------------------------------------------------------------
|
|
496
|
+
// Tools section
|
|
497
|
+
// ---------------------------------------------------------------------------
|
|
498
|
+
function groupToolsByPrefix(tools) {
|
|
499
|
+
if (tools.length < 3)
|
|
500
|
+
return [{ label: null, tools }];
|
|
501
|
+
// Count first-segment prefixes. A prefix earns a group when >= 2 tools share it.
|
|
502
|
+
const prefixCounts = new Map();
|
|
503
|
+
for (const tool of tools) {
|
|
504
|
+
const prefix = tool.name.split('_', 1)[0];
|
|
505
|
+
if (!prefix)
|
|
506
|
+
continue;
|
|
507
|
+
prefixCounts.set(prefix, (prefixCounts.get(prefix) ?? 0) + 1);
|
|
508
|
+
}
|
|
509
|
+
const groupablePrefixes = new Set([...prefixCounts.entries()].filter(([, count]) => count >= 2).map(([p]) => p));
|
|
510
|
+
if (groupablePrefixes.size === 0)
|
|
511
|
+
return [{ label: null, tools }];
|
|
512
|
+
// Preserve encounter order; create one group per qualifying prefix plus "Other".
|
|
513
|
+
const groups = new Map();
|
|
514
|
+
const other = [];
|
|
515
|
+
for (const tool of tools) {
|
|
516
|
+
const prefix = tool.name.split('_', 1)[0];
|
|
517
|
+
if (prefix && groupablePrefixes.has(prefix)) {
|
|
518
|
+
const list = groups.get(prefix) ?? [];
|
|
519
|
+
list.push(tool);
|
|
520
|
+
groups.set(prefix, list);
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
other.push(tool);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const out = [];
|
|
527
|
+
for (const [prefix, list] of groups) {
|
|
528
|
+
out.push({ label: titleCase(prefix), tools: list });
|
|
529
|
+
}
|
|
530
|
+
if (other.length > 0)
|
|
531
|
+
out.push({ label: 'Other', tools: other });
|
|
532
|
+
return out;
|
|
533
|
+
}
|
|
534
|
+
function titleCase(s) {
|
|
535
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
536
|
+
}
|
|
537
|
+
function buildInvocationSnippet(tool) {
|
|
538
|
+
const args = {};
|
|
539
|
+
for (const field of tool.requiredFields) {
|
|
540
|
+
args[field] = `<${field}>`;
|
|
541
|
+
}
|
|
542
|
+
return JSON.stringify({
|
|
543
|
+
jsonrpc: '2.0',
|
|
544
|
+
id: 1,
|
|
545
|
+
method: 'tools/call',
|
|
546
|
+
params: {
|
|
547
|
+
name: tool.name,
|
|
548
|
+
arguments: args,
|
|
549
|
+
},
|
|
550
|
+
}, null, 2);
|
|
551
|
+
}
|
|
552
|
+
function renderToolCard(tool) {
|
|
553
|
+
const anchor = `tool-${tool.name}`;
|
|
554
|
+
const annotations = tool.annotations;
|
|
555
|
+
const pills = [];
|
|
556
|
+
if (annotations?.readOnlyHint)
|
|
557
|
+
pills.push(renderPill('read-only', 'readonly'));
|
|
558
|
+
if (annotations?.destructiveHint === true)
|
|
559
|
+
pills.push(renderPill('destructive', 'destructive'));
|
|
560
|
+
if (annotations?.openWorldHint)
|
|
561
|
+
pills.push(renderPill('open-world', 'openworld'));
|
|
562
|
+
if (tool.isTask)
|
|
563
|
+
pills.push(renderPill('task', 'task'));
|
|
564
|
+
if (tool.isApp)
|
|
565
|
+
pills.push(renderPill('app', 'app'));
|
|
566
|
+
const source = tool.sourceUrl
|
|
567
|
+
? html `<a class="source-link" href="${tool.sourceUrl}" rel="noopener">view source ↗</a>`
|
|
568
|
+
: html ``;
|
|
569
|
+
const schemaPreview = tool.inputSchema
|
|
570
|
+
? html `
|
|
571
|
+
<details>
|
|
572
|
+
<summary>Input schema</summary>
|
|
573
|
+
<pre><code>${JSON.stringify(tool.inputSchema, null, 2)}</code></pre>
|
|
574
|
+
</details>
|
|
575
|
+
`
|
|
576
|
+
: html ``;
|
|
577
|
+
const invocation = buildInvocationSnippet(tool);
|
|
578
|
+
const authBadges = tool.auth && tool.auth.length > 0
|
|
579
|
+
? html `<div class="card-meta"><span class="card-meta-label">scopes:</span>${tool.auth.map((scope) => html ` <span class="pill pill-auth">${scope}</span>`)}</div>`
|
|
580
|
+
: html ``;
|
|
581
|
+
return html `
|
|
582
|
+
<article class="card" id="${anchor}">
|
|
583
|
+
<div class="card-head">
|
|
584
|
+
<h3 class="card-title"><a href="#${anchor}">${tool.name}</a></h3>
|
|
585
|
+
<div class="pill-row" role="list">${pills}</div>
|
|
586
|
+
${source}
|
|
587
|
+
</div>
|
|
588
|
+
<p class="card-desc">${tool.description}</p>
|
|
589
|
+
${authBadges}
|
|
590
|
+
${renderSnippet(`tool-${tool.name}`, invocation)}
|
|
591
|
+
${schemaPreview}
|
|
592
|
+
</article>
|
|
593
|
+
`;
|
|
594
|
+
}
|
|
595
|
+
function renderToolsSection(tools) {
|
|
596
|
+
if (tools.length === 0)
|
|
597
|
+
return html ``;
|
|
598
|
+
const groups = groupToolsByPrefix(tools);
|
|
599
|
+
const body = groups.map((group) => {
|
|
600
|
+
const heading = group.label ? html `<h4 class="group-heading">${group.label}</h4>` : html ``;
|
|
601
|
+
return html `${heading}${group.tools.map(renderToolCard)}`;
|
|
602
|
+
});
|
|
603
|
+
return html `
|
|
604
|
+
<section aria-labelledby="section-tools">
|
|
605
|
+
${renderSectionHeading('section-tools', 'Tools', tools.length)}
|
|
606
|
+
${body}
|
|
607
|
+
</section>
|
|
608
|
+
`;
|
|
609
|
+
}
|
|
610
|
+
// ---------------------------------------------------------------------------
|
|
611
|
+
// Resources section
|
|
612
|
+
// ---------------------------------------------------------------------------
|
|
613
|
+
function slugifyUri(template) {
|
|
614
|
+
return template
|
|
615
|
+
.replace(/[^a-z0-9]+/gi, '-')
|
|
616
|
+
.replace(/^-+|-+$/g, '')
|
|
617
|
+
.toLowerCase();
|
|
618
|
+
}
|
|
619
|
+
function renderResourceCard(resource) {
|
|
620
|
+
const anchor = `resource-${slugifyUri(resource.uriTemplate || resource.name)}`;
|
|
621
|
+
const source = resource.sourceUrl
|
|
622
|
+
? html `<a class="source-link" href="${resource.sourceUrl}" rel="noopener">view source ↗</a>`
|
|
623
|
+
: html ``;
|
|
624
|
+
return html `
|
|
625
|
+
<article class="card" id="${anchor}">
|
|
626
|
+
<div class="card-head">
|
|
627
|
+
<h3 class="card-title"><a href="#${anchor}">${resource.name}</a></h3>
|
|
628
|
+
${source}
|
|
629
|
+
</div>
|
|
630
|
+
<p class="card-desc">${resource.description}</p>
|
|
631
|
+
<div class="card-meta">
|
|
632
|
+
<span><span class="card-meta-label">uri:</span> <code>${resource.uriTemplate}</code></span>
|
|
633
|
+
${resource.mimeType ? html `<span><span class="card-meta-label">mime:</span> <code>${resource.mimeType}</code></span>` : html ``}
|
|
634
|
+
</div>
|
|
635
|
+
</article>
|
|
636
|
+
`;
|
|
637
|
+
}
|
|
638
|
+
function renderResourcesSection(resources) {
|
|
639
|
+
if (resources.length === 0)
|
|
640
|
+
return html ``;
|
|
641
|
+
return html `
|
|
642
|
+
<section aria-labelledby="section-resources">
|
|
643
|
+
${renderSectionHeading('section-resources', 'Resources', resources.length)}
|
|
644
|
+
${resources.map(renderResourceCard)}
|
|
645
|
+
</section>
|
|
646
|
+
`;
|
|
647
|
+
}
|
|
648
|
+
// ---------------------------------------------------------------------------
|
|
649
|
+
// Prompts section
|
|
650
|
+
// ---------------------------------------------------------------------------
|
|
651
|
+
function renderPromptCard(prompt) {
|
|
652
|
+
const anchor = `prompt-${prompt.name}`;
|
|
653
|
+
const source = prompt.sourceUrl
|
|
654
|
+
? html `<a class="source-link" href="${prompt.sourceUrl}" rel="noopener">view source ↗</a>`
|
|
655
|
+
: html ``;
|
|
656
|
+
const argsList = prompt.args.length > 0
|
|
657
|
+
? html `
|
|
658
|
+
<ul class="args-list">
|
|
659
|
+
${prompt.args.map((arg) => html `
|
|
660
|
+
<li>
|
|
661
|
+
<code>${arg.name}</code>${arg.required ? html `<span class="args-required">required</span>` : html ``}
|
|
662
|
+
${arg.description ? html ` — ${arg.description}` : html ``}
|
|
663
|
+
</li>
|
|
664
|
+
`)}
|
|
665
|
+
</ul>
|
|
666
|
+
`
|
|
667
|
+
: html ``;
|
|
668
|
+
return html `
|
|
669
|
+
<article class="card" id="${anchor}">
|
|
670
|
+
<div class="card-head">
|
|
671
|
+
<h3 class="card-title"><a href="#${anchor}">${prompt.name}</a></h3>
|
|
672
|
+
${source}
|
|
673
|
+
</div>
|
|
674
|
+
<p class="card-desc">${prompt.description}</p>
|
|
675
|
+
${argsList}
|
|
676
|
+
</article>
|
|
677
|
+
`;
|
|
678
|
+
}
|
|
679
|
+
function renderPromptsSection(prompts) {
|
|
680
|
+
if (prompts.length === 0)
|
|
681
|
+
return html ``;
|
|
682
|
+
return html `
|
|
683
|
+
<section aria-labelledby="section-prompts">
|
|
684
|
+
${renderSectionHeading('section-prompts', 'Prompts', prompts.length)}
|
|
685
|
+
${prompts.map(renderPromptCard)}
|
|
686
|
+
</section>
|
|
687
|
+
`;
|
|
688
|
+
}
|
|
689
|
+
// ---------------------------------------------------------------------------
|
|
690
|
+
// Extensions section
|
|
691
|
+
// ---------------------------------------------------------------------------
|
|
692
|
+
function renderExtensionsSection(extensions) {
|
|
693
|
+
if (!extensions || Object.keys(extensions).length === 0)
|
|
694
|
+
return html ``;
|
|
695
|
+
const entries = Object.entries(extensions);
|
|
696
|
+
return html `
|
|
697
|
+
<section aria-labelledby="section-extensions">
|
|
698
|
+
${renderSectionHeading('section-extensions', 'Extensions', entries.length)}
|
|
699
|
+
${entries.map(([key, value]) => html `
|
|
700
|
+
<article class="card ext-card">
|
|
701
|
+
<div class="card-head">
|
|
702
|
+
<h3 class="card-title ext-key">${key}</h3>
|
|
703
|
+
</div>
|
|
704
|
+
<pre class="ext-preview"><code>${JSON.stringify(value, null, 2)}</code></pre>
|
|
705
|
+
</article>
|
|
706
|
+
`)}
|
|
707
|
+
</section>
|
|
708
|
+
`;
|
|
709
|
+
}
|
|
710
|
+
// ---------------------------------------------------------------------------
|
|
711
|
+
// Footer
|
|
712
|
+
// ---------------------------------------------------------------------------
|
|
713
|
+
function renderFooter(manifest) {
|
|
714
|
+
const { landing, framework } = manifest;
|
|
715
|
+
const groups = [];
|
|
716
|
+
// User-supplied links
|
|
717
|
+
if (landing.links.length > 0) {
|
|
718
|
+
groups.push({
|
|
719
|
+
label: 'Links',
|
|
720
|
+
links: landing.links.map((l) => ({ href: l.href, label: l.label })),
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
// Auto-derived GitHub cluster
|
|
724
|
+
if (landing.repoRoot) {
|
|
725
|
+
const repo = landing.repoRoot;
|
|
726
|
+
const version = manifest.server.version;
|
|
727
|
+
groups.push({
|
|
728
|
+
label: 'Repository',
|
|
729
|
+
links: [
|
|
730
|
+
{ href: landing.changelogUrl ?? `${repo.url}/blob/main/CHANGELOG.md`, label: 'Changelog' },
|
|
731
|
+
{ href: `${repo.url}/releases/tag/v${version}`, label: `Release v${version}` },
|
|
732
|
+
{ href: `${repo.url}/issues`, label: 'Issues' },
|
|
733
|
+
{ href: repo.url, label: 'Source' },
|
|
734
|
+
],
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
// Package / registry
|
|
738
|
+
if (landing.npmPackage) {
|
|
739
|
+
groups.push({
|
|
740
|
+
label: 'Registry',
|
|
741
|
+
links: [{ href: landing.npmPackage.url, label: `npm: ${landing.npmPackage.name}` }],
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
const groupsHtml = groups.map((g) => html `
|
|
745
|
+
<div class="footer-group">
|
|
746
|
+
<span class="footer-group-label">${g.label}</span>
|
|
747
|
+
${g.links.map((l) => html `<a href="${l.href}" rel="noopener">${l.label}</a>`)}
|
|
748
|
+
</div>
|
|
749
|
+
`);
|
|
750
|
+
const frameworkNpm = `https://www.npmjs.com/package/${encodeURIComponent(framework.name)}`;
|
|
751
|
+
const attribution = landing.attribution
|
|
752
|
+
? html `<p class="footer-attrib">Built on <a href="${framework.homepage}">${framework.name}</a> v${framework.version} · <a href="${frameworkNpm}" rel="noopener">npm</a></p>`
|
|
753
|
+
: html ``;
|
|
754
|
+
return html `
|
|
755
|
+
<footer>
|
|
756
|
+
${groups.length > 0 ? html `<div class="footer-links">${groupsHtml}</div>` : html ``}
|
|
757
|
+
${attribution}
|
|
758
|
+
</footer>
|
|
759
|
+
`;
|
|
760
|
+
}
|
|
761
|
+
// ---------------------------------------------------------------------------
|
|
762
|
+
// <head> metadata
|
|
763
|
+
// ---------------------------------------------------------------------------
|
|
764
|
+
function renderHead(manifest, pageUrl) {
|
|
765
|
+
const { server, landing } = manifest;
|
|
766
|
+
const title = `${server.name} · MCP server`;
|
|
767
|
+
const description = landing.tagline ?? server.description ?? `MCP server: ${server.name}`;
|
|
768
|
+
const ogImage = landing.logo?.startsWith('http')
|
|
769
|
+
? html `<meta property="og:image" content="${landing.logo}" />`
|
|
770
|
+
: html ``;
|
|
771
|
+
const favicon = landing.logo && isImageDataUri(landing.logo)
|
|
772
|
+
? html `<link rel="icon" href="${landing.logo}" />`
|
|
773
|
+
: html ``;
|
|
774
|
+
const jsonLd = JSON.stringify({
|
|
775
|
+
'@context': 'https://schema.org',
|
|
776
|
+
'@type': 'SoftwareApplication',
|
|
777
|
+
name: server.name,
|
|
778
|
+
description,
|
|
779
|
+
applicationCategory: 'DeveloperApplication',
|
|
780
|
+
softwareVersion: server.version,
|
|
781
|
+
...(server.homepage && { url: server.homepage }),
|
|
782
|
+
});
|
|
783
|
+
return html `
|
|
784
|
+
<meta charset="utf-8" />
|
|
785
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
786
|
+
<title>${title}</title>
|
|
787
|
+
<meta name="description" content="${description}" />
|
|
788
|
+
<meta property="og:title" content="${title}" />
|
|
789
|
+
<meta property="og:description" content="${description}" />
|
|
790
|
+
<meta property="og:type" content="website" />
|
|
791
|
+
<meta property="og:url" content="${pageUrl}" />
|
|
792
|
+
${ogImage}
|
|
793
|
+
<meta name="twitter:card" content="summary" />
|
|
794
|
+
<meta name="twitter:title" content="${title}" />
|
|
795
|
+
<meta name="twitter:description" content="${description}" />
|
|
796
|
+
${favicon}
|
|
797
|
+
<link rel="mcp-endpoint" href="${manifest.transport.endpointPath}" />
|
|
798
|
+
<link rel="alternate" type="application/json" href="/.well-known/mcp.json" title="MCP Server Card" />
|
|
799
|
+
<script type="application/ld+json">${unsafeRaw(escapeLdJson(jsonLd))}</script>
|
|
800
|
+
`;
|
|
801
|
+
}
|
|
802
|
+
function isImageDataUri(value) {
|
|
803
|
+
if (!value)
|
|
804
|
+
return false;
|
|
805
|
+
return /^data:image\/(png|svg\+xml|jpeg|gif|webp|x-icon|vnd\.microsoft\.icon)/i.test(value);
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Escape a JSON string for safe embedding inside `<script type="application/ld+json">`.
|
|
809
|
+
* Replaces HTML-sensitive characters with Unicode escapes — `<` → `<`,
|
|
810
|
+
* `>` → `>`, `&` → `&`, U+2028/U+2029 line terminators → escaped.
|
|
811
|
+
* Matches the hardening recommendation in https://html.spec.whatwg.org/#script-data-state.
|
|
812
|
+
*/
|
|
813
|
+
function escapeLdJson(json) {
|
|
814
|
+
return json
|
|
815
|
+
.replace(/</g, '\\u003c')
|
|
816
|
+
.replace(/>/g, '\\u003e')
|
|
817
|
+
.replace(/&/g, '\\u0026')
|
|
818
|
+
.replace(/\u2028/g, '\\u2028')
|
|
819
|
+
.replace(/\u2029/g, '\\u2029');
|
|
820
|
+
}
|
|
821
|
+
// ---------------------------------------------------------------------------
|
|
822
|
+
// Page composition
|
|
823
|
+
// ---------------------------------------------------------------------------
|
|
824
|
+
/**
|
|
825
|
+
* Render the full landing page. `baseUrl` is the request origin
|
|
826
|
+
* (e.g. `https://pubmed.example.com`) — used in connect snippets and OG meta.
|
|
827
|
+
* `degraded` reduces the body when `requireAuth` gates unauth callers.
|
|
828
|
+
*/
|
|
829
|
+
export function renderLandingPage(manifest, baseUrl, degraded = false) {
|
|
830
|
+
const pageUrl = `${baseUrl.replace(/\/$/, '')}/`;
|
|
831
|
+
const landing = manifest.landing;
|
|
832
|
+
const body = degraded
|
|
833
|
+
? html `
|
|
834
|
+
${renderHero(manifest, baseUrl)}
|
|
835
|
+
<section>
|
|
836
|
+
<p class="empty-state">
|
|
837
|
+
Full server inventory is available to authenticated callers.
|
|
838
|
+
</p>
|
|
839
|
+
</section>
|
|
840
|
+
${renderFooter(manifest)}
|
|
841
|
+
`
|
|
842
|
+
: html `
|
|
843
|
+
${renderHero(manifest, baseUrl)}
|
|
844
|
+
${renderToolsSection(manifest.definitions.tools)}
|
|
845
|
+
${renderResourcesSection(manifest.definitions.resources)}
|
|
846
|
+
${renderPromptsSection(manifest.definitions.prompts)}
|
|
847
|
+
${renderExtensionsSection(manifest.extensions)}
|
|
848
|
+
${renderFooter(manifest)}
|
|
849
|
+
`;
|
|
850
|
+
const doc = html `<!DOCTYPE html>
|
|
851
|
+
<html lang="en">
|
|
852
|
+
<head>
|
|
853
|
+
${renderHead(manifest, pageUrl)}
|
|
854
|
+
${renderTokens(landing.theme.accent)}
|
|
855
|
+
</head>
|
|
856
|
+
<body>
|
|
857
|
+
<main>${body}</main>
|
|
858
|
+
${renderCopyScript()}
|
|
859
|
+
</body>
|
|
860
|
+
</html>`;
|
|
861
|
+
return doc.toString();
|
|
862
|
+
}
|
|
863
|
+
// ---------------------------------------------------------------------------
|
|
864
|
+
// Hono handler
|
|
865
|
+
// ---------------------------------------------------------------------------
|
|
866
|
+
/**
|
|
867
|
+
* Factory for the `GET /` route handler.
|
|
868
|
+
*
|
|
869
|
+
* Cache behavior and body shape depend on `manifest.landing.requireAuth`:
|
|
870
|
+
*
|
|
871
|
+
* | `requireAuth` | Authenticated | Unauthenticated |
|
|
872
|
+
* |:---|:---|:---|
|
|
873
|
+
* | `false` (default) | full page · `Cache-Control: public, max-age=60` | full page · same |
|
|
874
|
+
* | `true` | full page · `Cache-Control: private, max-age=60` · `Vary: Authorization` | reduced hero-only page · same cache headers |
|
|
875
|
+
*
|
|
876
|
+
* The check is header-presence based — we don't validate the bearer token
|
|
877
|
+
* here (that's the MCP endpoint's job). If a caller presents any Authorization
|
|
878
|
+
* header, the full inventory renders; if not, they see a stub and a pointer
|
|
879
|
+
* to the docs link when available.
|
|
880
|
+
*/
|
|
881
|
+
export function createLandingPageHandler(manifest) {
|
|
882
|
+
return (c) => {
|
|
883
|
+
const context = requestContextService.createRequestContext({
|
|
884
|
+
operation: 'landingPageHandler',
|
|
885
|
+
});
|
|
886
|
+
const url = new URL(c.req.url);
|
|
887
|
+
const baseUrl = url.origin;
|
|
888
|
+
const requireAuth = manifest.landing.requireAuth;
|
|
889
|
+
const authHeader = c.req.header('authorization');
|
|
890
|
+
const isAuthenticated = Boolean(authHeader && authHeader.trim().length > 0);
|
|
891
|
+
const degraded = requireAuth && !isAuthenticated;
|
|
892
|
+
const html = renderLandingPage(manifest, baseUrl, degraded);
|
|
893
|
+
logger.debug('Serving landing page.', {
|
|
894
|
+
...context,
|
|
895
|
+
accept: c.req.header('accept'),
|
|
896
|
+
bytes: html.length,
|
|
897
|
+
requireAuth,
|
|
898
|
+
degraded,
|
|
899
|
+
});
|
|
900
|
+
c.header('Content-Type', 'text/html; charset=utf-8');
|
|
901
|
+
c.header('X-Content-Type-Options', 'nosniff');
|
|
902
|
+
if (requireAuth) {
|
|
903
|
+
c.header('Cache-Control', 'private, max-age=60');
|
|
904
|
+
c.header('Vary', 'Authorization');
|
|
905
|
+
}
|
|
906
|
+
else {
|
|
907
|
+
c.header('Cache-Control', 'public, max-age=60');
|
|
908
|
+
}
|
|
909
|
+
return c.body(html);
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
//# sourceMappingURL=landing-page.js.map
|