@vantageos/mcp-boilerplate 0.2.3 → 0.2.4
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-plugin/plugin.json +1 -1
- package/CHANGELOG.md +14 -0
- package/README.fr.md +5 -0
- package/README.md +4 -0
- package/dist/oauth/authorizationServer.js +1 -0
- package/dist/oauth/picker.d.ts +2 -0
- package/dist/oauth/picker.js +56 -9
- package/dist/oauth/settings.d.ts +2 -0
- package/dist/oauth/settings.js +2 -0
- package/examples/oauth.env.example +2 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vantage-mcp-boilerplate",
|
|
3
3
|
"displayName": "VantageOS MCP Boilerplate",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.4",
|
|
5
5
|
"description": "Clonable MCP server template: SDK v2 on protocol 2026-07-28 (stdio + stateless Streamable HTTP), its own OAuth 2.1 authorization server (Clerk person + organisation), MCP Apps UI, FR/EN i18n and a declarative eval harness. Scaffold, extend, publish.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ElPi Corp / Laurent Perello"
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.4] - 2026-10-11
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- The consent / organisation picker page uses the VantagePeers dashboard design tokens: dark by
|
|
15
|
+
default, light under `prefers-color-scheme`, one option card per organisation with its role
|
|
16
|
+
(`admin`, not `(org:admin)`), a primary Approve button. It still loads no external asset; the
|
|
17
|
+
single inline style is hashed into the page CSP.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- `AUTHORIZE_BRAND_NAME` (optional): a display name shown above the consent page title. None by
|
|
22
|
+
default, so a fork carries no brand it did not set.
|
|
23
|
+
|
|
10
24
|
## [0.2.3] - 2026-10-11
|
|
11
25
|
|
|
12
26
|
### Added
|
package/README.fr.md
CHANGED
|
@@ -194,6 +194,11 @@ fonctionne sur un serveur que vous hébergez, dans cet ordre :
|
|
|
194
194
|
nomme l'exigence de Home URL. C'est un avertissement, jamais un refus : il signifie « une
|
|
195
195
|
connexion distante ne marchera que si la Home URL pointe sur ce serveur, ou si vous passez à
|
|
196
196
|
une instance de production ». Il ne contient ni clé ni secret.
|
|
197
|
+
6. **La page de consentement.** Le choix d'organisation reprend le design du tableau de bord
|
|
198
|
+
VantagePeers : sombre par défaut, clair quand le système de la personne le demande, une
|
|
199
|
+
carte par organisation avec son rôle. Elle ne charge aucune ressource externe et porte donc
|
|
200
|
+
ses propres tokens. Réglez `AUTHORIZE_BRAND_NAME` pour afficher le nom de votre produit
|
|
201
|
+
au-dessus du titre ; rien n'est affiché par défaut.
|
|
197
202
|
|
|
198
203
|
### Multi-tenance
|
|
199
204
|
|
package/README.md
CHANGED
|
@@ -183,6 +183,10 @@ server you host, in this order:
|
|
|
183
183
|
logs one warning at boot, in English and French, naming the Home URL requirement. It is a
|
|
184
184
|
warning, never a refusal: it means "a remote sign-in will only work if the Home URL points
|
|
185
185
|
at this server, or if you move to a production instance". It carries no key or secret.
|
|
186
|
+
6. **The consent page.** The organisation picker uses the VantagePeers dashboard design: dark
|
|
187
|
+
by default, light when the person's system asks for it, one card per organisation with its
|
|
188
|
+
role. It loads no external asset, so it carries its own tokens. Set `AUTHORIZE_BRAND_NAME`
|
|
189
|
+
to show your product's name above the title; nothing is shown by default.
|
|
186
190
|
|
|
187
191
|
### Multi-tenancy
|
|
188
192
|
|
|
@@ -195,6 +195,7 @@ export function createAuthorizationServer(deps = {}) {
|
|
|
195
195
|
return c.html(renderOrgPicker(outcome.model, locale, {
|
|
196
196
|
// Only a URL-shaped (CIMD) client_id is bound to a domain its owner controls.
|
|
197
197
|
unverified: !isUrlShapedClientId(outcome.model.client.clientId),
|
|
198
|
+
brandName: env[OAUTH_ENV.brandName]?.trim() || undefined,
|
|
198
199
|
}));
|
|
199
200
|
}
|
|
200
201
|
case "refused": {
|
package/dist/oauth/picker.d.ts
CHANGED
|
@@ -12,5 +12,7 @@ export declare function pickerContentSecurityPolicy(): Promise<string>;
|
|
|
12
12
|
export interface ConsentContext {
|
|
13
13
|
/** True for a Dynamic Client Registration client: it vouched for itself, nobody else did. */
|
|
14
14
|
unverified: boolean;
|
|
15
|
+
/** The deployment's display name (AUTHORIZE_BRAND_NAME), shown above the title; none by default. */
|
|
16
|
+
brandName?: string;
|
|
15
17
|
}
|
|
16
18
|
export declare function renderOrgPicker(model: OrgPickerModel, locale: McpLocale, consent: ConsentContext): string;
|
package/dist/oauth/picker.js
CHANGED
|
@@ -13,9 +13,54 @@ function escapeHtml(value) {
|
|
|
13
13
|
.replace(/"/g, """)
|
|
14
14
|
.replace(/'/g, "'");
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
/**
|
|
17
|
+
* The VantagePeers dashboard design tokens (vantage-peers-dashboard styles/presets/base.css at
|
|
18
|
+
* 9ca86e8a): dark first, light under `prefers-color-scheme`. Copied as values because the page
|
|
19
|
+
* loads no external asset (CSP below); a change to the dashboard preset is carried here by hand.
|
|
20
|
+
* Geist is the dashboard font; with no external fonts allowed, the system stack stands in.
|
|
21
|
+
*/
|
|
22
|
+
const PICKER_STYLE = [
|
|
23
|
+
":root{color-scheme:dark light;--background:oklch(0.08 0.005 265);--foreground:oklch(0.95 0.005 265);" +
|
|
24
|
+
"--card:oklch(0.11 0.005 265);--card-hover:oklch(0.14 0.005 265);--primary:oklch(0.95 0.005 265);" +
|
|
25
|
+
"--primary-foreground:oklch(0.08 0.005 265);--secondary:oklch(0.16 0.005 265);" +
|
|
26
|
+
"--muted-foreground:oklch(0.55 0.005 265);--border:oklch(0.22 0.005 265);" +
|
|
27
|
+
"--border-hover:oklch(0.30 0.005 265);--ring:oklch(0.55 0.005 265);--warning:oklch(0.75 0.18 85);" +
|
|
28
|
+
"--radius:0.75rem}",
|
|
29
|
+
"@media (prefers-color-scheme:light){:root{--background:oklch(0.98 0.003 265);" +
|
|
30
|
+
"--foreground:oklch(0.10 0.005 265);--card:oklch(1 0 0);--card-hover:oklch(0.96 0.003 265);" +
|
|
31
|
+
"--primary:oklch(0.10 0.005 265);--primary-foreground:oklch(0.98 0.003 265);" +
|
|
32
|
+
"--secondary:oklch(0.94 0.003 265);--muted-foreground:oklch(0.45 0.005 265);" +
|
|
33
|
+
"--border:oklch(0.88 0.003 265);--border-hover:oklch(0.78 0.003 265);--ring:oklch(0.45 0.005 265);" +
|
|
34
|
+
"--warning:oklch(0.65 0.18 85)}}",
|
|
35
|
+
"*{box-sizing:border-box}",
|
|
36
|
+
"body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:1.5rem;" +
|
|
37
|
+
"background:var(--background);color:var(--foreground);" +
|
|
38
|
+
"font:15px/1.55 ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif}",
|
|
39
|
+
"main{width:100%;max-width:28rem;background:var(--card);border:1px solid var(--border);" +
|
|
40
|
+
"border-radius:var(--radius);padding:2rem}",
|
|
41
|
+
".brand{margin:0 0 1.25rem;font-size:.8rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;" +
|
|
42
|
+
"color:var(--muted-foreground)}",
|
|
43
|
+
"h1{margin:0 0 .5rem;font-size:1.25rem;font-weight:600;letter-spacing:-.01em}",
|
|
44
|
+
"p{margin:0 0 1rem}.muted{color:var(--muted-foreground);font-size:.875rem}",
|
|
45
|
+
"code{font-family:ui-monospace,'SF Mono',monospace;font-size:.8125rem;background:var(--secondary);" +
|
|
46
|
+
"padding:.1rem .35rem;border-radius:.375rem}",
|
|
47
|
+
".alert{border:1px solid var(--warning);border-radius:.5rem;padding:.75rem;color:var(--warning);font-size:.875rem}",
|
|
48
|
+
"fieldset{border:0;margin:0 0 1.5rem;padding:0}",
|
|
49
|
+
"legend{font-size:.8rem;font-weight:600;color:var(--muted-foreground);margin-bottom:.5rem;padding:0}",
|
|
50
|
+
".org{display:flex;align-items:center;gap:.75rem;padding:.75rem .875rem;margin-bottom:.5rem;" +
|
|
51
|
+
"border:1px solid var(--border);border-radius:.5rem;cursor:pointer}",
|
|
52
|
+
".org:hover{border-color:var(--border-hover);background:var(--card-hover)}",
|
|
53
|
+
".org:has(input:checked){border-color:var(--primary)}",
|
|
54
|
+
".org input{accent-color:var(--primary);margin:0}",
|
|
55
|
+
".name{flex:1;font-weight:500}",
|
|
56
|
+
".role{font-size:.75rem;color:var(--muted-foreground);background:var(--secondary);" +
|
|
57
|
+
"padding:.125rem .5rem;border-radius:999px}",
|
|
58
|
+
".actions{display:flex;gap:.5rem}",
|
|
59
|
+
"button{flex:1;font:inherit;font-weight:500;padding:.625rem 1rem;border-radius:.5rem;cursor:pointer;" +
|
|
60
|
+
"border:1px solid var(--border);background:transparent;color:var(--foreground)}",
|
|
61
|
+
"button.primary{background:var(--primary);color:var(--primary-foreground);border-color:var(--primary)}",
|
|
62
|
+
"button:hover{opacity:.9}button:focus-visible,.org:focus-within{outline:2px solid var(--ring);outline-offset:2px}",
|
|
63
|
+
].join("");
|
|
19
64
|
/** CSP for the picker page: no scripts, no external assets, one hashed inline style. */
|
|
20
65
|
export async function pickerContentSecurityPolicy() {
|
|
21
66
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(PICKER_STYLE));
|
|
@@ -30,29 +75,31 @@ export async function pickerContentSecurityPolicy() {
|
|
|
30
75
|
export function renderOrgPicker(model, locale, consent) {
|
|
31
76
|
const destination = escapeHtml(model.redirectOrigin);
|
|
32
77
|
const unverified = consent.unverified
|
|
33
|
-
? `<p role="alert"><strong>${escapeHtml(t(locale, "oauth.pickerUnverified"))}</strong></p>\n`
|
|
78
|
+
? `<p class="alert" role="alert"><strong>${escapeHtml(t(locale, "oauth.pickerUnverified"))}</strong></p>\n`
|
|
34
79
|
: "";
|
|
35
80
|
const clientName = model.client.clientName ?? model.client.clientId;
|
|
36
81
|
const only = model.organizations.length === 1;
|
|
37
82
|
const orgs = model.organizations
|
|
38
|
-
.map((o, i) => `<label><input type="radio" name="orgId" value="${escapeHtml(o.id)}" required${only && i === 0 ? " checked" : ""}
|
|
83
|
+
.map((o, i) => `<label class="org"><input type="radio" name="orgId" value="${escapeHtml(o.id)}" required${only && i === 0 ? " checked" : ""}><span class="name">${escapeHtml(o.name)}</span><span class="role">${escapeHtml(o.role.replace(/^org:/, ""))}</span></label>`)
|
|
39
84
|
.join("\n");
|
|
40
85
|
const title = escapeHtml(t(locale, "oauth.pickerTitle").replace("{client}", clientName));
|
|
41
86
|
const intro = escapeHtml(t(locale, "oauth.pickerIntro").replaceAll("{client}", clientName));
|
|
87
|
+
const brand = consent.brandName ? `<p class="brand">${escapeHtml(consent.brandName)}</p>\n` : "";
|
|
42
88
|
return `<!doctype html>
|
|
43
89
|
<html lang="${locale}">
|
|
44
90
|
<head>
|
|
45
91
|
<meta charset="utf-8">
|
|
46
92
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
47
93
|
<meta name="referrer" content="no-referrer">
|
|
94
|
+
<meta name="color-scheme" content="dark light">
|
|
48
95
|
<title>${title}</title>
|
|
49
96
|
<style>${PICKER_STYLE}</style>
|
|
50
97
|
</head>
|
|
51
98
|
<body>
|
|
52
99
|
<main>
|
|
53
|
-
<h1>${title}</h1>
|
|
100
|
+
${brand}<h1>${title}</h1>
|
|
54
101
|
<p>${intro}</p>
|
|
55
|
-
<p>${escapeHtml(t(locale, "oauth.pickerDestination"))} <strong>${destination}</strong><br>
|
|
102
|
+
<p class="muted">${escapeHtml(t(locale, "oauth.pickerDestination"))} <strong>${destination}</strong><br>
|
|
56
103
|
${escapeHtml(t(locale, "oauth.pickerScope"))} <code>${escapeHtml(model.scope)}</code></p>
|
|
57
104
|
${unverified}<form method="post" action="/authorize/org">
|
|
58
105
|
<input type="hidden" name="state" value="${escapeHtml(model.state)}">
|
|
@@ -61,8 +108,8 @@ ${unverified}<form method="post" action="/authorize/org">
|
|
|
61
108
|
<legend>${escapeHtml(t(locale, "oauth.pickerOrganisation"))}</legend>
|
|
62
109
|
${orgs}
|
|
63
110
|
</fieldset>
|
|
64
|
-
<p>
|
|
65
|
-
<button type="submit" name="approved" value="true">${escapeHtml(t(locale, "oauth.approve"))}</button>
|
|
111
|
+
<p class="actions">
|
|
112
|
+
<button class="primary" type="submit" name="approved" value="true">${escapeHtml(t(locale, "oauth.approve"))}</button>
|
|
66
113
|
<button type="submit" name="approved" value="false" formnovalidate>${escapeHtml(t(locale, "oauth.deny"))}</button>
|
|
67
114
|
</p>
|
|
68
115
|
</form>
|
package/dist/oauth/settings.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export declare const OAUTH_ENV: {
|
|
|
20
20
|
readonly registerCorsOrigins: "MCP_REGISTER_CORS_ORIGINS";
|
|
21
21
|
/** Exactly "1" lets NODE_ENV=production run on the in-memory stores (single instance only). */
|
|
22
22
|
readonly allowInMemoryStores: "MCP_ALLOW_IN_MEMORY_STORES";
|
|
23
|
+
/** Optional display name shown above the consent page title (default: none). */
|
|
24
|
+
readonly brandName: "AUTHORIZE_BRAND_NAME";
|
|
23
25
|
};
|
|
24
26
|
/** Scope granted when a client names none: MCP access plus the OIDC subject. */
|
|
25
27
|
export declare const MCP_SCOPE = "mcp:full";
|
package/dist/oauth/settings.js
CHANGED
|
@@ -20,6 +20,8 @@ export const OAUTH_ENV = {
|
|
|
20
20
|
registerCorsOrigins: "MCP_REGISTER_CORS_ORIGINS",
|
|
21
21
|
/** Exactly "1" lets NODE_ENV=production run on the in-memory stores (single instance only). */
|
|
22
22
|
allowInMemoryStores: "MCP_ALLOW_IN_MEMORY_STORES",
|
|
23
|
+
/** Optional display name shown above the consent page title (default: none). */
|
|
24
|
+
brandName: "AUTHORIZE_BRAND_NAME",
|
|
23
25
|
};
|
|
24
26
|
/** Scope granted when a client names none: MCP access plus the OIDC subject. */
|
|
25
27
|
export const MCP_SCOPE = "mcp:full";
|
|
@@ -33,3 +33,5 @@ MCP_OAUTH_SIGNING_JWK=
|
|
|
33
33
|
# MCP_MAX_BODY_BYTES=1048576
|
|
34
34
|
# Browser origins allowed to call /register cross-origin (comma-separated; default none).
|
|
35
35
|
# MCP_REGISTER_CORS_ORIGINS=https://inspector.example.com
|
|
36
|
+
# Display name shown above the consent page title (default: none).
|
|
37
|
+
# AUTHORIZE_BRAND_NAME=Acme MCP
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vantageos/mcp-boilerplate",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Clonable MCP server skeleton on the official @modelcontextprotocol/server SDK v2 (protocol 2026-07-28 + 2025 legacy, stdio + stateless Streamable HTTP), with its own OAuth 2.1 authorization server (Clerk person + organisation, cloud-identity), MCP Apps UI, tenant scoping, FR/EN i18n and a declarative eval harness.",
|
|
5
5
|
"description_fr": "Squelette de serveur MCP clonable sur le SDK officiel @modelcontextprotocol/server v2 (protocole 2026-07-28 + héritage 2025, stdio + Streamable HTTP sans état), avec son propre serveur d'autorisation OAuth 2.1 (personne + organisation Clerk, cloud-identity), UI MCP Apps, scoping tenant, i18n FR/EN et un harnais d'évaluations déclaratif.",
|
|
6
6
|
"type": "module",
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "vantage-mcp-boilerplate",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.4",
|
|
5
5
|
"description": "Clonable MCP server template: SDK v2 on protocol 2026-07-28 (stdio + stateless Streamable HTTP), its own OAuth 2.1 authorization server (Clerk person + organisation), MCP Apps UI, FR/EN i18n and a declarative eval harness. Scaffold, extend, publish.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ElPi Corp / Laurent Perello"
|