@vantageos/mcp-boilerplate 0.5.0 → 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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vantage-mcp-boilerplate",
3
3
  "displayName": "VantageOS MCP Boilerplate",
4
- "version": "0.5.0",
4
+ "version": "0.6.0",
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,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.0] - 2026-11-01
11
+
12
+ ### Added
13
+ - `mcp-readme-fr` (bin, `scripts/readme-fr.mjs`): generates `README.fr.md` from a bilingual `README.md` whose passages carry explicit `<!-- lang:en -->` / `<!-- lang:fr -->` / `<!-- /lang -->` markers (mcp-standard C-1: one French file, never hand-copied). `--check` fails when the two diverge, a malformed marker is an error naming its line, and a hand-written `README.fr.md` (no banner) is never overwritten. `tests/readmeFr.test.ts`.
14
+
15
+ ### Fixed
16
+ - The CHANGELOG link block covers every version: a GitHub compare or tag link where a tag exists (v0.1.1, v0.1.2, v0.3.0, v0.4.0), the npm page of the version otherwise (0.1.0, 0.2.x, 0.5.0; the dead `v0.1.0` tag link is replaced). `[Unreleased]` compares the newest tag, v0.4.0. `tests/changelogLinks.test.ts` fails when a `## [x]` heading has no link line, a link has no heading, or a URL is of no known form.
17
+
10
18
  ## [0.5.0] - 2026-10-29
11
19
 
12
20
  > **Breaking for multi-instance deployments.** Cursors are now signed under a key drawn once per
@@ -479,6 +487,16 @@ Initial published release.
479
487
  - Reproducible build via `npm ci`; 52/52 tests passing at release time.
480
488
  - MIT license, clonable as a starter template.
481
489
 
482
- [Unreleased]: https://github.com/elpiarthera/vantage-mcp-boilerplate/compare/v0.1.1...HEAD
483
- [0.1.1]: https://github.com/elpiarthera/vantage-mcp-boilerplate/compare/v0.1.0...v0.1.1
484
- [0.1.0]: https://github.com/elpiarthera/vantage-mcp-boilerplate/releases/tag/v0.1.0
490
+ [Unreleased]: https://github.com/elpiarthera/vantage-mcp-boilerplate/compare/v0.4.0...HEAD
491
+ [0.6.0]: https://www.npmjs.com/package/@vantageos/mcp-boilerplate/v/0.6.0
492
+ [0.5.0]: https://www.npmjs.com/package/@vantageos/mcp-boilerplate/v/0.5.0
493
+ [0.4.0]: https://github.com/elpiarthera/vantage-mcp-boilerplate/compare/v0.3.0...v0.4.0
494
+ [0.3.0]: https://github.com/elpiarthera/vantage-mcp-boilerplate/compare/v0.1.2...v0.3.0
495
+ [0.2.4]: https://www.npmjs.com/package/@vantageos/mcp-boilerplate/v/0.2.4
496
+ [0.2.3]: https://www.npmjs.com/package/@vantageos/mcp-boilerplate/v/0.2.3
497
+ [0.2.2]: https://www.npmjs.com/package/@vantageos/mcp-boilerplate/v/0.2.2
498
+ [0.2.1]: https://www.npmjs.com/package/@vantageos/mcp-boilerplate/v/0.2.1
499
+ [0.2.0]: https://www.npmjs.com/package/@vantageos/mcp-boilerplate/v/0.2.0
500
+ [0.1.2]: https://github.com/elpiarthera/vantage-mcp-boilerplate/compare/v0.1.1...v0.1.2
501
+ [0.1.1]: https://github.com/elpiarthera/vantage-mcp-boilerplate/releases/tag/v0.1.1
502
+ [0.1.0]: https://www.npmjs.com/package/@vantageos/mcp-boilerplate/v/0.1.0
package/README.fr.md CHANGED
@@ -517,6 +517,24 @@ le client envoie `X-MCP-Locale: fr|en` (défaut `en`) ; le sélecteur navigateur
517
517
  convention `<Namespace>.<category>.<key>` de `@vantageos/mosaic-i18n`, FR et EN en
518
518
  parité.
519
519
 
520
+ ## Un README français généré depuis un README bilingue
521
+
522
+ Un serveur dont le `README.md` porte l'anglais et le français côte à côte livre un `README.fr.md` généré depuis lui
523
+ (mcp-standard C-1), jamais recopié à la main. Marquez chaque passage bilingue, puis lancez le générateur :
524
+
525
+ ```markdown
526
+ <!-- lang:en -->
527
+ A demo server.
528
+ <!-- lang:fr -->
529
+ Un serveur de démonstration.
530
+ <!-- /lang -->
531
+ ```
532
+
533
+ Le texte hors d'une région marquée (code, tableaux, liens) est copié dans les deux. `npx mcp-readme-fr --write` écrit
534
+ `README.fr.md` ; `npx mcp-readme-fr --check` (à mettre dans `npm test`) échoue quand les deux divergent. Un marqueur
535
+ mal formé est une erreur qui nomme sa ligne, et le générateur n'écrase jamais un `README.fr.md` écrit à la main (sans
536
+ sa bannière). Ce dépôt garde sa propre traduction écrite à la main.
537
+
520
538
  ## Tests
521
539
 
522
540
  ```bash
package/README.md CHANGED
@@ -489,6 +489,24 @@ client sends `X-MCP-Locale: fr|en` (default `en`); the browser picker follows
489
489
  `<Namespace>.<category>.<key>` convention of `@vantageos/mosaic-i18n`, FR and EN
490
490
  in lockstep.
491
491
 
492
+ ## A French README generated from a bilingual one
493
+
494
+ A server whose `README.md` carries English and French side by side ships a `README.fr.md` generated from it
495
+ (mcp-standard C-1), never hand-copied. Mark each bilingual passage, then run the generator:
496
+
497
+ ```markdown
498
+ <!-- lang:en -->
499
+ A demo server.
500
+ <!-- lang:fr -->
501
+ Un serveur de démonstration.
502
+ <!-- /lang -->
503
+ ```
504
+
505
+ Text outside a marked region (code, tables, links) is copied to both. `npx mcp-readme-fr --write` writes
506
+ `README.fr.md`; `npx mcp-readme-fr --check` (put it in `npm test`) fails when the two diverge. A malformed marker is
507
+ an error naming its line, and the generator never overwrites a hand-written `README.fr.md` (one without its banner).
508
+ This repository keeps its own hand translation.
509
+
492
510
  ## Testing
493
511
 
494
512
  ```bash
package/mcp-server.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-boilerplate",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "capabilities": {
5
5
  "tools": {},
6
6
  "resources": {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vantageos/mcp-boilerplate",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
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",
@@ -18,7 +18,8 @@
18
18
  "./package.json": "./package.json"
19
19
  },
20
20
  "bin": {
21
- "mcp-boilerplate": "./dist/stdio.js"
21
+ "mcp-boilerplate": "./dist/stdio.js",
22
+ "mcp-readme-fr": "./scripts/readme-fr.mjs"
22
23
  },
23
24
  "files": [
24
25
  "dist",
@@ -36,7 +37,8 @@
36
37
  "scripts/generate-signing-key.mjs",
37
38
  "docs",
38
39
  "examples/claude-desktop.json",
39
- "examples/oauth.env.example"
40
+ "examples/oauth.env.example",
41
+ "scripts/readme-fr.mjs"
40
42
  ],
41
43
  "publishConfig": {
42
44
  "access": "public"
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.5.0",
4
+ "version": "0.6.0",
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"
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * readme-fr — generate README.fr.md from a bilingual README.md (mcp-standard C-1: a French reader gets one French
4
+ * file). The French text is written once, in README.md, next to the English; README.fr.md is derived, never
5
+ * hand-copied, and `--check` fails when the two diverge.
6
+ *
7
+ * The language of a passage is read from an explicit marker, never guessed from its words. Census of the forms:
8
+ * <!-- lang:en --> opens a bilingual region; the lines that follow are its English part (dropped)
9
+ * <!-- lang:fr --> switches the open region to its French part (kept)
10
+ * <!-- /lang --> closes the region
11
+ * A marker stands alone on its line. Text outside a region is shared and copied as is (code blocks, tables, links).
12
+ * A fenced code block (``` or ~~~) is copied verbatim with the part it sits in; a marker inside it is text. An
13
+ * unclosed fence is an error naming its opening line.
14
+ * Errors, each naming its line (exit 2): an unknown language, a nested region, a French part before the English one,
15
+ * a region closed with no French part, a close with no open region, a region left open at the end of the file.
16
+ *
17
+ * Usage:
18
+ * mcp-readme-fr --check [--dir <path>] regenerate in memory; exit 1 naming the first differing line
19
+ * mcp-readme-fr --write [--dir <path>] write README.fr.md; refuses (exit 2) to overwrite a README.fr.md that
20
+ * does not start with the generated-file banner (a hand translation)
21
+ * No dependencies: node built-ins only.
22
+ */
23
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
24
+ import { join, resolve } from "node:path";
25
+ import { fileURLToPath } from "node:url";
26
+
27
+ export const BANNER =
28
+ "<!-- Généré depuis README.md par mcp-readme-fr (@vantageos/mcp-boilerplate) : ne pas modifier à la main. -->";
29
+
30
+ const MARKER = /^\s*<!--\s*(lang:([A-Za-z-]+)|\/lang)\s*-->\s*$/;
31
+ const FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
32
+
33
+ /** README.md text -> { fr, errors }. `fr` is the README.fr.md content (banner first). */
34
+ export function generate(text) {
35
+ const out = [BANNER, ""];
36
+ const errors = [];
37
+ let state = "shared"; // shared | en | fr
38
+ let openedAt = 0;
39
+ const lines = text.split("\n");
40
+ let fence = null; // { char, len, at } while inside a fenced code block
41
+ lines.forEach((line, i) => {
42
+ const n = i + 1;
43
+ // A fenced code block (``` or ~~~, CommonMark: up to 3 spaces of indent) is copied verbatim with the part it
44
+ // sits in: a marker inside it is text, never a region.
45
+ const f = FENCE.exec(line);
46
+ if (fence !== null) {
47
+ if (state !== "en") out.push(line);
48
+ if (f && f[1][0] === fence.char && f[1].length >= fence.len && f[2].trim() === "")
49
+ fence = null;
50
+ return;
51
+ }
52
+ if (f) {
53
+ fence = { char: f[1][0], len: f[1].length, at: n };
54
+ if (state !== "en") out.push(line);
55
+ return;
56
+ }
57
+ const m = MARKER.exec(line);
58
+ if (!m) {
59
+ if (state !== "en") out.push(line);
60
+ return;
61
+ }
62
+ if (m[1] === "/lang") {
63
+ if (state === "shared") errors.push(`line ${n}: <!-- /lang --> closes no open region`);
64
+ else if (state === "en")
65
+ errors.push(
66
+ `line ${n}: the region opened at line ${openedAt} has no French part (<!-- lang:fr -->)`
67
+ );
68
+ state = "shared";
69
+ return;
70
+ }
71
+ const lang = m[2].toLowerCase();
72
+ if (lang !== "en" && lang !== "fr") {
73
+ errors.push(`line ${n}: unknown language marker lang:${m[2]} (known: en, fr)`);
74
+ return;
75
+ }
76
+ if (lang === "en") {
77
+ if (state !== "shared")
78
+ errors.push(
79
+ `line ${n}: nested region (the region opened at line ${openedAt} is still open)`
80
+ );
81
+ state = "en";
82
+ openedAt = n;
83
+ return;
84
+ }
85
+ // lang === "fr"
86
+ if (state === "shared")
87
+ errors.push(
88
+ `line ${n}: lang:fr opens a region before its English part (<!-- lang:en --> first)`
89
+ );
90
+ else if (state === "fr")
91
+ errors.push(`line ${n}: a second lang:fr in the region opened at line ${openedAt}`);
92
+ state = "fr";
93
+ });
94
+ if (fence !== null)
95
+ errors.push(`line ${fence.at}: unclosed code fence (${fence.char.repeat(fence.len)})`);
96
+ if (state !== "shared")
97
+ errors.push(
98
+ `line ${lines.length}: the region opened at line ${openedAt} is unclosed (<!-- /lang -->)`
99
+ );
100
+ return { fr: out.join("\n"), errors };
101
+ }
102
+
103
+ export function main(argv, cwd = process.cwd()) {
104
+ const mode = argv.includes("--write") ? "write" : argv.includes("--check") ? "check" : null;
105
+ const dirAt = argv.indexOf("--dir");
106
+ const dir = resolve(cwd, dirAt >= 0 ? (argv[dirAt + 1] ?? ".") : ".");
107
+ if (mode === null) {
108
+ console.error("mcp-readme-fr: --check or --write is required");
109
+ return 2;
110
+ }
111
+ const readmePath = join(dir, "README.md");
112
+ const frPath = join(dir, "README.fr.md");
113
+ if (!existsSync(readmePath)) {
114
+ console.error(`mcp-readme-fr: ${readmePath} not found`);
115
+ return 2;
116
+ }
117
+ const { fr, errors } = generate(readFileSync(readmePath, "utf8"));
118
+ if (errors.length > 0) {
119
+ for (const e of errors) console.error(`README.md ${e}`);
120
+ return 2;
121
+ }
122
+ const current = existsSync(frPath) ? readFileSync(frPath, "utf8") : null;
123
+ if (mode === "write") {
124
+ if (current !== null && !current.startsWith(BANNER)) {
125
+ console.error(
126
+ `mcp-readme-fr: ${frPath} was not generated (no banner): refusing to overwrite a hand translation`
127
+ );
128
+ return 2;
129
+ }
130
+ writeFileSync(frPath, fr);
131
+ console.log(`mcp-readme-fr: wrote ${frPath}`);
132
+ return 0;
133
+ }
134
+ if (current === fr) {
135
+ console.log("mcp-readme-fr --check: README.fr.md matches README.md");
136
+ return 0;
137
+ }
138
+ const a = (current ?? "").split("\n");
139
+ const b = fr.split("\n");
140
+ let i = 0;
141
+ while (i < Math.max(a.length, b.length) && a[i] === b[i]) i++;
142
+ console.error(
143
+ `mcp-readme-fr --check: README.fr.md differs from what README.md generates at line ${i + 1} — run mcp-readme-fr --write`
144
+ );
145
+ return 1;
146
+ }
147
+
148
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
149
+ process.exit(main(process.argv.slice(2)));
150
+ }