@vantageos/mcp-boilerplate 0.6.3 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +27 -0
  3. package/README.fr.md +97 -5
  4. package/README.md +93 -7
  5. package/dist/hostVerification.d.ts +15 -0
  6. package/dist/hostVerification.js +32 -0
  7. package/dist/http.js +4 -1
  8. package/dist/httpApp.d.ts +9 -0
  9. package/dist/httpApp.js +55 -2
  10. package/dist/listCursor.d.ts +10 -0
  11. package/dist/listCursor.js +37 -0
  12. package/dist/locales/en.json +12 -1
  13. package/dist/locales/fr.json +12 -1
  14. package/dist/mcpServerFactory.d.ts +11 -2
  15. package/dist/mcpServerFactory.js +97 -15
  16. package/dist/oauth/authorizationServer.js +33 -12
  17. package/dist/oauth/picker.js +1 -0
  18. package/dist/oauth/settings.d.ts +2 -0
  19. package/dist/oauth/settings.js +2 -0
  20. package/dist/oauth/tokenIssuer.d.ts +0 -1
  21. package/dist/oauth/tokenIssuer.js +1 -18
  22. package/dist/pagination.d.ts +2 -1
  23. package/dist/pagination.js +2 -1
  24. package/dist/rateLimit.d.ts +33 -0
  25. package/dist/rateLimit.js +48 -0
  26. package/dist/serverManifest.d.ts +9 -1
  27. package/dist/serverManifest.js +10 -1
  28. package/dist/signedCursor.js +6 -1
  29. package/dist/stdio.js +2 -2
  30. package/dist/tenancy.d.ts +4 -14
  31. package/dist/tenancy.js +7 -18
  32. package/dist/toolRecord.d.ts +35 -2
  33. package/dist/toolRecord.js +44 -2
  34. package/dist/toolRoleGate.d.ts +41 -0
  35. package/dist/toolRoleGate.js +94 -0
  36. package/dist/toolRoles.d.ts +57 -0
  37. package/dist/toolRoles.js +113 -0
  38. package/dist/ui/sandboxDomain.d.ts +13 -0
  39. package/dist/ui/sandboxDomain.js +23 -0
  40. package/dist/upstream/paginatedClient.js +1 -0
  41. package/dist/verifiedPerson.d.ts +6 -0
  42. package/dist/verifiedPerson.js +9 -0
  43. package/docs/releasing.md +19 -0
  44. package/mcp-server.json +3 -2
  45. package/package.json +10 -3
  46. package/plugin.json +1 -1
  47. package/scripts/plugin-manifest.source.json +19 -0
  48. package/scripts/readme-fr.mjs +3 -1
  49. package/dist/oauth/clientAssertion.d.ts +0 -28
  50. package/dist/oauth/clientAssertion.js +0 -106
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vantageos/mcp-boilerplate",
3
- "version": "0.6.3",
3
+ "version": "0.7.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",
@@ -53,11 +53,15 @@
53
53
  "homepage": "https://github.com/elpiarthera/vantage-mcp-boilerplate#readme",
54
54
  "author": "ElPi Corp / Laurent Perello",
55
55
  "scripts": {
56
- "build": "node scripts/clean-dist.mjs && tsc && mkdir -p dist/locales && cp src/locales/en.json src/locales/fr.json dist/locales/ && node scripts/sync-plugin-version.mjs",
56
+ "build": "node scripts/clean-dist.mjs && tsc && mkdir -p dist/locales && cp src/locales/en.json src/locales/fr.json dist/locales/ && node scripts/sync-plugin-version.mjs && node scripts/listing-kit.mjs --check",
57
+ "listing:write": "node scripts/listing-kit.mjs",
58
+ "changelog:check": "node scripts/changelog-assemble.mjs --check",
59
+ "changelog:release": "node scripts/changelog-assemble.mjs",
57
60
  "prepack": "node scripts/check-dist-orphans.mjs",
58
61
  "build:ui": "vite build && mv dist/ui-bundle/index.html dist/ui-bundle/widget.html",
59
62
  "pretest": "npm run build && npm run build:ui",
60
63
  "test": "vitest run",
64
+ "test:a11y": "vitest run tests/widgetAxe.test.ts",
61
65
  "test:watch": "vitest",
62
66
  "typecheck": "tsc --noEmit",
63
67
  "start:http": "node dist/http.js",
@@ -83,7 +87,7 @@
83
87
  "dependencies": {
84
88
  "@modelcontextprotocol/ext-apps": "^2.0.3",
85
89
  "@modelcontextprotocol/server": "^2.3.1",
86
- "@vantageos/cloud-identity": "^0.20.2",
90
+ "@vantageos/cloud-identity": "^0.34.1",
87
91
  "@vantageos/mosaic-i18n": "^0.1.1",
88
92
  "hono": "^4.13.13",
89
93
  "jose": "^6.2.7",
@@ -100,11 +104,14 @@
100
104
  "devDependencies": {
101
105
  "@biomejs/biome": "1.9.4",
102
106
  "@modelcontextprotocol/client": "^2.3.1",
107
+ "@types/jsdom": "^30.0.0",
103
108
  "@types/node": "^20.14.0",
104
109
  "@types/react": "^19.0.0",
105
110
  "@types/react-dom": "^19.0.0",
106
111
  "@vitejs/plugin-react": "^4.3.0",
112
+ "axe-core": "^4.14.0",
107
113
  "convex": "1.46.0",
114
+ "jsdom": "^30.1.3",
108
115
  "react": "^19.0.0",
109
116
  "react-dom": "^19.0.0",
110
117
  "typescript": "^5.7.2",
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.6.3",
4
+ "version": "0.7.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"
@@ -31,5 +31,24 @@
31
31
  "openai": {
32
32
  "auth": { "type": "oauth", "client": { "mode": "cimd" } },
33
33
  "capabilities": ["Read"]
34
+ },
35
+ "listing": {
36
+ "testPrompts": {
37
+ "positive": [
38
+ "tools-call-ping-happy-path",
39
+ "tools-call-ping-default",
40
+ "tools-call-list-items-public-tier-unscoped",
41
+ "tools-call-list-items-page-truncated",
42
+ "tools-call-list-items-ui-link"
43
+ ],
44
+ "negative": [
45
+ "tools-call-ping-message-too-long-failure",
46
+ "tools-call-list-items-requires-tenant-refusal-failure",
47
+ "tools-call-list-items-invalid-cursor-failure"
48
+ ]
49
+ },
50
+ "reviewerAccount": {
51
+ "howToObtain": "Sign in at the server's /authorize page with any Clerk account; a reviewer organisation is created on request to the maintainers (README, 'Own authorization server'). No credential is stored in this repository."
52
+ }
34
53
  }
35
54
  }
@@ -27,7 +27,9 @@ import { fileURLToPath } from "node:url";
27
27
  export const BANNER =
28
28
  "<!-- Généré depuis README.md par mcp-readme-fr (@vantageos/mcp-boilerplate) : ne pas modifier à la main. -->";
29
29
 
30
- const MARKER = /^\s*<!--\s*(lang:([A-Za-z-]+)|\/lang)\s*-->\s*$/;
30
+ // A marker sits at the start of a line with at most 3 spaces of indent (CommonMark): 4 or more spaces, or a tab, make
31
+ // the line an indented code block, so such a "marker" is text, copied with the part it sits in, like a fenced one.
32
+ const MARKER = /^ {0,3}<!--\s*(lang:([A-Za-z-]+)|\/lang)\s*-->\s*$/;
31
33
  const FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
32
34
 
33
35
  /** README.md text -> { fr, errors }. `fr` is the README.fr.md content (banner first). */
@@ -1,28 +0,0 @@
1
- import { type CimdOptions } from "./clientRegistry.js";
2
- export declare const JWT_BEARER_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
3
- export declare const ASSERTION_SIGNING_ALGS: readonly ["RS256"];
4
- export type AssertionResult = {
5
- ok: true;
6
- clientId: string;
7
- } | {
8
- ok: false;
9
- reason: string;
10
- };
11
- /** Bounded set of accepted jti values, each held until its assertion expires. */
12
- export declare function createJtiStore(maxEntries?: number): {
13
- /** True when the jti is new (and is now recorded); false on a replay or when full. */
14
- accept(clientId: string, jti: string, exp: number, nowSeconds: number): boolean;
15
- };
16
- export interface AssertionVerifierOptions extends CimdOptions {
17
- now?: () => number;
18
- jtiStore?: ReturnType<typeof createJtiStore>;
19
- }
20
- export declare function createClientAssertionVerifier(options?: AssertionVerifierOptions): {
21
- verify(input: {
22
- assertionType: string | undefined;
23
- assertion: string | undefined;
24
- /** The `client_id` request parameter, when sent. */
25
- clientId: string | undefined;
26
- audiences: string[];
27
- }): Promise<AssertionResult>;
28
- };
@@ -1,106 +0,0 @@
1
- /**
2
- * private_key_jwt client authentication at `/token` (RFC 7523 / OIDC Core 9).
3
- *
4
- * A presented assertion is VERIFIED, never ignored and never trusted unverified:
5
- * - the key set comes from the `jwks_uri` of the client's CIMD document, fetched through the
6
- * same SSRF guard as the document itself;
7
- * - alg is pinned to RS256 (a document declaring another signing alg is refused), so `none`
8
- * and HMAC assertions cannot verify;
9
- * - `iss` and `sub` equal the client_id; `aud` is this token endpoint or this issuer; `exp`
10
- * is required (30 s skew); `jti` is required and accepted ONCE.
11
- */
12
- import { createLocalJWKSet, decodeJwt, jwtVerify } from "jose";
13
- import { fetchGuardedJson, isUrlShapedClientId, resolveCimdDocument, } from "./clientRegistry.js";
14
- export const JWT_BEARER_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
15
- export const ASSERTION_SIGNING_ALGS = ["RS256"];
16
- const SKEW_SECONDS = 30;
17
- const MAX_JWKS_BYTES = 64 * 1024;
18
- /** Bounded set of accepted jti values, each held until its assertion expires. */
19
- export function createJtiStore(maxEntries = 10_000) {
20
- const seen = new Map(); // `${clientId}\n${jti}` -> exp (epoch seconds)
21
- return {
22
- /** True when the jti is new (and is now recorded); false on a replay or when full. */
23
- accept(clientId, jti, exp, nowSeconds) {
24
- for (const [k, e] of seen)
25
- if (e + SKEW_SECONDS <= nowSeconds)
26
- seen.delete(k);
27
- const key = `${clientId}\n${jti}`;
28
- if (seen.has(key))
29
- return false;
30
- if (seen.size >= maxEntries)
31
- return false; // fail closed: never evict a live jti
32
- seen.set(key, exp);
33
- return true;
34
- },
35
- };
36
- }
37
- export function createClientAssertionVerifier(options = {}) {
38
- const now = options.now ?? (() => Date.now());
39
- const jtiStore = options.jtiStore ?? createJtiStore();
40
- const fetchOpts = {
41
- fetchImpl: options.fetchImpl,
42
- resolveHost: options.resolveHost,
43
- maxBytes: MAX_JWKS_BYTES,
44
- ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
45
- };
46
- return {
47
- async verify(input) {
48
- const fail = (reason) => ({ ok: false, reason });
49
- if (input.assertionType !== JWT_BEARER_ASSERTION_TYPE || !input.assertion) {
50
- return fail("unsupported client assertion");
51
- }
52
- // The unverified iss only LOCATES the key set; it is re-checked against the verified claims.
53
- let claimedIss;
54
- try {
55
- claimedIss = decodeJwt(input.assertion).iss;
56
- }
57
- catch {
58
- return fail("malformed client assertion");
59
- }
60
- if (!claimedIss || !isUrlShapedClientId(claimedIss))
61
- return fail("unknown client");
62
- if (input.clientId !== undefined && input.clientId !== claimedIss) {
63
- return fail("client_id does not match the assertion issuer");
64
- }
65
- const clientId = claimedIss;
66
- const cimd = await resolveCimdDocument(clientId, options);
67
- if (!cimd?.jwksUri)
68
- return fail("client has no verifiable key set");
69
- if (cimd.signingAlg !== undefined &&
70
- !ASSERTION_SIGNING_ALGS.includes(cimd.signingAlg)) {
71
- return fail("unsupported signing algorithm");
72
- }
73
- let jwksUrl;
74
- try {
75
- jwksUrl = new URL(cimd.jwksUri);
76
- }
77
- catch {
78
- return fail("invalid jwks_uri");
79
- }
80
- const jwks = (await fetchGuardedJson(jwksUrl, fetchOpts));
81
- if (!jwks || !Array.isArray(jwks.keys))
82
- return fail("key set unavailable");
83
- let payload;
84
- try {
85
- const verified = await jwtVerify(input.assertion, createLocalJWKSet(jwks), {
86
- algorithms: [...ASSERTION_SIGNING_ALGS],
87
- issuer: clientId,
88
- subject: clientId,
89
- audience: input.audiences,
90
- clockTolerance: SKEW_SECONDS,
91
- currentDate: new Date(now()),
92
- requiredClaims: ["iss", "sub", "aud", "exp", "jti"],
93
- });
94
- payload = verified.payload;
95
- }
96
- catch {
97
- return fail("client assertion rejected");
98
- }
99
- const exp = payload.exp;
100
- if (!payload.jti || !jtiStore.accept(clientId, payload.jti, exp, Math.floor(now() / 1000))) {
101
- return fail("client assertion replayed");
102
- }
103
- return { ok: true, clientId };
104
- },
105
- };
106
- }