@vantageos/mcp-boilerplate 0.6.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +39 -0
- package/README.fr.md +146 -10
- package/README.md +141 -12
- package/dist/build-info.json +1 -0
- package/dist/buildIdentity.d.ts +16 -0
- package/dist/buildIdentity.js +39 -0
- package/dist/embedded.generated.d.ts +225 -0
- package/dist/embedded.generated.js +8 -0
- package/dist/hostVerification.d.ts +15 -0
- package/dist/hostVerification.js +32 -0
- package/dist/http.js +9 -4
- package/dist/httpApp.d.ts +15 -0
- package/dist/httpApp.js +119 -9
- package/dist/i18n.js +6 -16
- package/dist/landingPage.d.ts +24 -0
- package/dist/landingPage.js +109 -0
- package/dist/listCursor.d.ts +10 -0
- package/dist/listCursor.js +37 -0
- package/dist/mcpServerFactory.d.ts +22 -2
- package/dist/mcpServerFactory.js +125 -22
- package/dist/oauth/authorizationServer.js +33 -12
- package/dist/oauth/picker.js +1 -0
- package/dist/oauth/settings.d.ts +2 -0
- package/dist/oauth/settings.js +2 -0
- package/dist/oauth/tokenIssuer.d.ts +0 -1
- package/dist/oauth/tokenIssuer.js +1 -18
- package/dist/pagination.d.ts +2 -1
- package/dist/pagination.js +2 -1
- package/dist/rateLimit.d.ts +33 -0
- package/dist/rateLimit.js +48 -0
- package/dist/servedToolRecords.d.ts +6 -0
- package/dist/servedToolRecords.js +11 -1
- package/dist/serverCard.d.ts +68 -0
- package/dist/serverCard.js +68 -0
- package/dist/serverIcon.d.ts +17 -0
- package/dist/serverIcon.js +26 -0
- package/dist/serverIdentity.d.ts +83 -0
- package/dist/serverIdentity.js +130 -0
- package/dist/serverManifest.d.ts +9 -1
- package/dist/serverManifest.js +10 -1
- package/dist/signedCursor.js +6 -1
- package/dist/stdio.js +2 -2
- package/dist/tenancy.d.ts +4 -14
- package/dist/tenancy.js +7 -18
- package/dist/toolRecord.d.ts +35 -2
- package/dist/toolRecord.js +44 -2
- package/dist/toolRoleGate.d.ts +41 -0
- package/dist/toolRoleGate.js +94 -0
- package/dist/toolRoles.d.ts +57 -0
- package/dist/toolRoles.js +113 -0
- package/dist/ui/sandboxDomain.d.ts +13 -0
- package/dist/ui/sandboxDomain.js +23 -0
- package/dist/ui/uiResource.d.ts +14 -1
- package/dist/ui/uiResource.js +11 -1
- package/dist/upstream/paginatedClient.js +1 -0
- package/dist/verifiedPerson.d.ts +6 -0
- package/dist/verifiedPerson.js +9 -0
- package/dist/version.js +6 -5
- package/docs/manufact-gap.md +200 -0
- package/docs/releasing.md +19 -0
- package/mcp-server.json +3 -2
- package/package.json +11 -3
- package/plugin.json +2 -4
- package/scripts/plugin-manifest.source.json +19 -0
- package/scripts/readme-fr.mjs +11 -4
- package/dist/locales/en.json +0 -77
- package/dist/locales/fr.json +0 -77
- package/dist/oauth/clientAssertion.d.ts +0 -28
- package/dist/oauth/clientAssertion.js +0 -106
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vantage-mcp-boilerplate",
|
|
3
3
|
"displayName": "VantageOS MCP Boilerplate",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.8.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,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.0] - 2026-11-10
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `docs/manufact-gap.md`: the gap list of this boilerplate against every Manufact quality-score check (31 checks, server metadata, discoverability, policy review, domain), each with its verdict, the mcp-doctor rule, the evidence and the file that owns the fix.
|
|
14
|
+
- Server metadata and discoverability (mcp-server-standard S-49, S-50; Manufact "Server Metadata" and "Discoverability"): `serverInfo` serves `title`, `description` (English, or French on `X-MCP-Locale: fr`), `websiteUrl` and a vector icon served at `/icon.svg`, all derived from `package.json` (`displayName`, `description`, `description_fr`, `homepage`) or the `MCP_SERVER_*` variables (`.env.example`); the instructions say how to use the tools instead of pointing to the README; the MCP Server Card is served without credential at `/mcp/server-card`, `/.well-known/mcp/server-card.json`, `/.well-known/mcp-server-card`, `/mcp.json` and `/.well-known/mcp.json`; a browser `GET /mcp` gets a bilingual setup page naming the MCP URL, ahead of the auth gate. Build identity (C-25): `/health` and `serverInfo` `buildId` is now the git commit the tree was built from (`dist/build-info.json`, written by `npm run build`), and the hash of the built files moves to `buildHash`.
|
|
15
|
+
- **The view resource describes itself.** The `ui://` resource carries a bilingual `widgetDescription` (EN / FR) under `_meta.ui.widgetDescription` and `_meta["openai/widgetDescription"]`, the two keys Manufact's *tool-resource-metadata-complete* check reads (T2, docs/manufact-gap.md). The text lives in `src/locales/{en,fr}.json` (`widget.description`).
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **The package imports from a bundle: locales, version and identity fields are module values, never a file read at import.** `src/i18n.ts` read `locales/{en,fr}.json` and `src/version.ts` read `package.json` when imported, so a bundled consumer (a Convex action importing `@vantageos/lojii-client`, which depends on this package) failed with ENOENT before any code ran. `scripts/embed-static.mjs` now writes `src/embedded.generated.ts` from those files at build (`--check` refuses a stale copy); `getServerIdentity()` reads the embedded package fields too. Proven by importing the built `dist/` from a directory with no `locales/` and no `package.json` (tests/i18n-bundled.test.ts). Declared, unchanged: the server's runtime assets (mcp-server.json, the widget bundle, tool roles, the build-tree hash) are still read on demand when a server starts, not at import. (README: #server-side-i18n-fren)
|
|
19
|
+
- `mcp-readme-fr` prints every line number 1-based: a region opened by `lang:fr` without its English part was reported as "opened at line 0", and an unclosed region at the end of a file ending with a newline named the empty line after it as the last line.
|
|
20
|
+
|
|
21
|
+
## [0.7.0] - 2026-11-02
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- Every tool record declares its `domain` and `mode` (`read` | `write`, agreeing with `readOnlyHint`), served in its annotations (mcp-server-standard "Agent tool roles", rule 2): `ping` is `system` / `read`, `list_items` is `items` / `read`. The start refuses a tool missing either, or with another mode, or whose mode disagrees with `readOnlyHint`, naming it.
|
|
25
|
+
- **Agent tool roles enforced on the OAuth tier** (mcp-server-standard "Agent tool roles"): every tool is granted or refused by the caller's role through `@vantageos/cloud-identity` `decideToolRole`, at both points — `tools/list` holds only the granted tools, and a call to any other tool is `isError: true` with `TOOL_ROLE_DENIED` / `TOOL_ROLE_UNRESOLVED`, its handler never run. The OAuth start now requires both a role store (`MCP_ROLES_FILE`, format in `examples/roles.example.json`, or an injected `toolRoles`) and the identity stores, and is refused naming each missing piece. A caller the identity layer did not resolve is refused with its `person_unresolved` code before any tool runs, `ping` included. New identity tool `whoami` (outside the gate): who the caller is, the role, the granted tools, `rolesApplicable`. The declared global context ceiling rises 1200 -> 1400 for this third tool.
|
|
26
|
+
- Tool invocation rate limiting (mcp-server-standard S-24): a token bucket per caller, `TOOL_RATE_LIMIT_BURST` (default 30) and `TOOL_RATE_LIMIT_PER_MINUTE` (default 60); a call past the limit is a tool error `rate_limited` with `retryAfterMs`; `withRateLimit(…)` wraps every tool handler.
|
|
27
|
+
- The directory listing kit `listing/listing.json` (mcp-standard C-35): description, served tools with titles and hints, 5 positive and 3 negative test prompts by eval id, and how a reviewer obtains an account; written by `npm run listing:write` from `scripts/plugin-manifest.source.json` and the served `tools/list`; `npm run build` runs `scripts/listing-kit.mjs --check` and fails on a stale committed kit, as does `tests/listingKit.test.ts`.
|
|
28
|
+
- `mcp-server.json` declares the target hosts (`distribution.targets`: claude, claude-code, vscode, cursor, chatgpt; mcp-server-standard S-45), and each declared host's verification path is served from configuration: `/.well-known/openai-apps-challenge` from `OPENAI_APPS_CHALLENGE`, `/.well-known/google-connected-apps` from `GOOGLE_CONNECTED_APPS_VERIFICATION`; unset, a 404 naming the variable.
|
|
29
|
+
- An axe-core accessibility gate (mcp-app-standard A-4): `tests/widgetAxe.test.ts`, run by `npm run test:a11y` and by `npm test` (so by `prepublishOnly`), audits the shipped `dist/ui-bundle/widget.html` against WCAG 2.1 A/AA in both locales, empty and populated, and fails on any serious or critical violation; colour contrast is not proven (jsdom computes no layout). Dev dependencies: `axe-core`, `jsdom`, `@types/jsdom`.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- The changelog is written as one fragment per pull request in `changelog.d/`, folded into `CHANGELOG.md` at release by `npm run changelog:release -- --version <x.y.z>`; `## [Unreleased]` stays empty and `npm run changelog:check` (part of `npm test`) refuses an entry written there. The entries accumulated under `## [Unreleased]` since 0.6.3 moved into fragments (one duplicated, stale entry dropped). `docs/releasing.md` describes the release.
|
|
33
|
+
- The widget's `_meta.ui.domain` is derived from the served MCP URL (`PUBLIC_BASE_URL` + `/mcp`): the first 32 hex characters of its SHA-256 followed by `.claudemcpcontent.com` (mcp-app-standard A-11), in `src/ui/sandboxDomain.ts`. `MCP_UI_DOMAIN` is removed: a typed domain went stale the moment the URL changed. Without `PUBLIC_BASE_URL`, or on stdio, no domain is declared.
|
|
34
|
+
- `@vantageos/cloud-identity` ^0.20.2 -> ^0.33.1. `/userinfo` verifies our own access token with cloud-identity's `verifyMcpAccessToken` (issuer = this server, audience = the protected resource, key set = the published JWKS) instead of a local `jwtVerify`; `TokenIssuer` only signs. The tenant is read by cloud-identity's `requireTenantFromIdentity` instead of a local `org_id` read. DLocalIdentity: 6 identity sites leave the boilerplate.
|
|
35
|
+
- The `private_key_jwt` client-assertion verifier comes from `@vantageos/cloud-identity/client-assertion` (^0.34.1, which caps an assertion's lifetime at 300 s); the boilerplate's own copy, `src/oauth/clientAssertion.ts`, is removed. DLocalIdentity: 0 local identity primitives.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
- A cursor sent to `tools/list`, `resources/list` or `resources/templates/list` is refused with JSON-RPC -32602 Invalid params (mcp-standard C-11 (a)); the SDK served those lists as one page and silently answered page 1. `list_items` now declares its default page size in its served `inputSchema`.
|
|
39
|
+
- The C-11 (a) cursor guard also covers `prompts/list`: a fork that registers a prompt gets -32602 on a cursor its single-page list never issued, as for `tools/list`, `resources/list` and `resources/templates/list` (review correction on #52).
|
|
40
|
+
- The served descriptions of `ping` and `list_items` carry both languages in their one field ("<en> / <fr>") and their trigger phrase (mcp-standard C-19); `ping` was English-only on the wire. Their declared context estimates rise to cover the longer text (`ping` 280, `list_items` 690 tokens), within the ceilings.
|
|
41
|
+
- `scripts/readme-fr.mjs` reads a language marker only at 0 to 3 spaces of indent: a line indented 4 or more spaces, or with a tab, is an indented code block (CommonMark), so a marker there is text, copied with the part it sits in.
|
|
42
|
+
- The tools' output schemas stay open to added fields (mcp-server-standard S-44): `ping`, `list_items` and its items are `z.looseObject`, so a field added in a later release no longer fails a client that validated against the schema it read at connect time.
|
|
43
|
+
|
|
44
|
+
### Security
|
|
45
|
+
- `/token` is rate limited per client address (`MCP_TOKEN_LIMIT_PER_MINUTE`, default 60; five times that per IPv6 /48; `429` with `Retry-After: 60`), so a caller cannot fill the single-use assertion slots and lock assertion-authenticated clients out.
|
|
46
|
+
|
|
10
47
|
## [0.6.3] - 2026-11-01
|
|
11
48
|
|
|
12
49
|
### Fixed
|
|
@@ -506,6 +543,8 @@ Initial published release.
|
|
|
506
543
|
- MIT license, clonable as a starter template.
|
|
507
544
|
|
|
508
545
|
[Unreleased]: https://github.com/elpiarthera/vantage-mcp-boilerplate/compare/v0.4.0...HEAD
|
|
546
|
+
[0.8.0]: https://www.npmjs.com/package/@vantageos/mcp-boilerplate/v/0.8.0
|
|
547
|
+
[0.7.0]: https://www.npmjs.com/package/@vantageos/mcp-boilerplate/v/0.7.0
|
|
509
548
|
[0.6.3]: https://www.npmjs.com/package/@vantageos/mcp-boilerplate/v/0.6.3
|
|
510
549
|
[0.6.2]: https://www.npmjs.com/package/@vantageos/mcp-boilerplate/v/0.6.2
|
|
511
550
|
[0.6.1]: https://www.npmjs.com/package/@vantageos/mcp-boilerplate/v/0.6.1
|
package/README.fr.md
CHANGED
|
@@ -23,7 +23,10 @@ src/
|
|
|
23
23
|
toolRecord.ts # fiche d'outil typée : autorité, verbe, table, mécanisme, pagination, coût
|
|
24
24
|
pagination.ts # pagination par curseur appliquée par l'outil (défaut, max fini, invalid_cursor)
|
|
25
25
|
signedCursor.ts # primitive de curseur signé HMAC (createCursorSigner), utilisée par tout curseur
|
|
26
|
-
buildIdentity.ts # identité de build
|
|
26
|
+
buildIdentity.ts # identité de build : le commit git construit (buildId) + le hachage des fichiers construits (buildHash)
|
|
27
|
+
serverIdentity.ts # titre, description, site, icône, instructions : package.json ou env
|
|
28
|
+
serverCard.ts # la MCP Server Card publique
|
|
29
|
+
landingPage.ts # la page d'installation servie à un navigateur sur /mcp
|
|
27
30
|
stdio.ts # serveStdio(factory) : les deux ères du protocole sur stdio
|
|
28
31
|
http.ts # adaptateur Node http.Server autour de l'app Hono
|
|
29
32
|
httpApp.ts # createMcpHandler(factory) sur /mcp + la barrière OAuth
|
|
@@ -112,6 +115,13 @@ de liste du protocole, servies par le SDK.
|
|
|
112
115
|
échouent au moindre manque. Les annotations sont des indications non fiables pour le
|
|
113
116
|
client (mcp-standard C-15) : ne conditionnez jamais une action sur elles seules.
|
|
114
117
|
|
|
118
|
+
Chaque appel d'outil est limité en débit (mcp-server-standard S-24) : un seau à jetons par appelant (l'organisation
|
|
119
|
+
résolue, ou `public` sur le palier non cloisonné), partagé par le processus. `TOOL_RATE_LIMIT_BURST` appels peuvent
|
|
120
|
+
partir en rafale (30 par défaut), puis le seau se remplit à `TOOL_RATE_LIMIT_PER_MINUTE` (60 par défaut). Un appel
|
|
121
|
+
au-delà de la limite est une erreur d'outil portant `structuredContent.error.code` `rate_limited` et `retryAfterMs`
|
|
122
|
+
(message FR/EN) ; une limite qui n'est pas un entier strictement positif arrête le serveur au démarrage. Un nouvel
|
|
123
|
+
outil enveloppe son handler avec `withRateLimit(…)`.
|
|
124
|
+
|
|
115
125
|
## Paliers d'authentification
|
|
116
126
|
|
|
117
127
|
| `AUTH_MODE` | Comportement |
|
|
@@ -168,7 +178,11 @@ Pour la mesurer, envoyez 21 enregistrements depuis un même client et attendez u
|
|
|
168
178
|
21e. Un client IPv6 est compté sur son /64 ET sur son /48 : tous les /64 d'un même /48 se
|
|
169
179
|
partagent `MCP_REGISTER_LIMIT_PER_48_PER_MINUTE` enregistrements par minute (défaut `100`),
|
|
170
180
|
si bien qu'un /48, qui contient 65536 /64, ne peut pas remplir le registre de clients jusqu'à
|
|
171
|
-
son `503`. Une valeur invalide arrête le serveur au démarrage.
|
|
181
|
+
son `503`. Une valeur invalide arrête le serveur au démarrage. `/token` est limité de la même façon, sur la
|
|
182
|
+
même adresse : `MCP_TOKEN_LIMIT_PER_MINUTE` requêtes par minute (défaut `60`), cinq fois plus par /48 IPv6,
|
|
183
|
+
puis `429` avec `Retry-After: 60`. Chaque assertion client `private_key_jwt` qu'il vérifie occupe une place à
|
|
184
|
+
usage unique jusqu'à son expiration (300 s au plus, plafond de la couche d'identité) : un appelant sans limite
|
|
185
|
+
pourrait remplir ces places et bloquer tous les clients authentifiés par assertion ; la limite l'empêche.
|
|
172
186
|
|
|
173
187
|
**Limites de requête et CORS** : Une variable d'exploitation définie mais invalide (par exemple `MCP_MAX_BODY_BYTES=abc`) arrête le serveur au démarrage avec le nom de la variable et la forme attendue, jamais un défaut silencieux ; non définie ou vide, le défaut s'applique. l'entrée Node lit au plus `MCP_MAX_BODY_BYTES`
|
|
174
188
|
(défaut 1 Mio) du corps d'une requête et répond `413` avant tout handler.
|
|
@@ -315,11 +329,39 @@ server.registerTool("my_rows", config, withVerifiedPerson(deps, async (args, per
|
|
|
315
329
|
|
|
316
330
|
Sauf si la personne est résolue, le handler ne s'exécute pas : l'appel reçoit une erreur d'outil
|
|
317
331
|
MCP au code stable `person_unresolved` (`structuredContent.error.code`), un message FR/EN selon
|
|
318
|
-
`X-MCP-Locale`, et une `reason` : `identity_not_configured` (aucune `identity` injectée
|
|
332
|
+
`X-MCP-Locale`, et une `reason` : `identity_not_configured` (aucune `identity` injectée — sur le
|
|
333
|
+
palier public seulement, puisque le palier OAuth refuse de démarrer sans elle, voir plus bas),
|
|
319
334
|
`no_verified_session` (palier public, et **stdio, qui n'a jamais de personne** : ce transport ne
|
|
320
335
|
porte aucun bearer) ou `refused` (le paquet a refusé ; son `code` et sa `reason` sont transmis,
|
|
321
|
-
jamais une valeur de claim).
|
|
322
|
-
|
|
336
|
+
jamais une valeur de claim). Sur le palier public et en stdio, un outil qui n'enveloppe pas son
|
|
337
|
+
handler s'exécute sans personne. Sur le palier OAuth, aucun outil ne s'exécute pour un appelant que
|
|
338
|
+
la couche d'identité n'a pas résolu : le contrôle des rôles ci-dessous répond d'abord, avec ce même
|
|
339
|
+
refus.
|
|
340
|
+
|
|
341
|
+
### Rôles d'outils des agents
|
|
342
|
+
|
|
343
|
+
Sur le palier OAuth, chaque outil est accordé ou refusé selon le RÔLE de l'appelant, par
|
|
344
|
+
`decideToolRole` de `@vantageos/cloud-identity` (mcp-server-standard « Agent tool roles »). Chaque
|
|
345
|
+
outil déclare son `domain` et son `mode` (voir « Ajouter un outil ») ; un rôle accorde la lecture
|
|
346
|
+
et/ou l'écriture par domaine, plus `add` (outils accordés hors de ses domaines) et `remove` (outils
|
|
347
|
+
retirés, qui l'emporte toujours). La même décision sert aux deux points : `tools/list` ne contient
|
|
348
|
+
que les outils accordés, et l'appel de tout autre outil reçoit un résultat `isError: true` au
|
|
349
|
+
`structuredContent.error.code` `TOOL_ROLE_DENIED` (`TOOL_ROLE_UNRESOLVED` quand le stockage des
|
|
350
|
+
rôles n'a pas pu être lu), sans que son handler s'exécute.
|
|
351
|
+
|
|
352
|
+
Le démarrage est refusé, en nommant chaque pièce manquante, sauf si les deux sont présents :
|
|
353
|
+
|
|
354
|
+
- **les stores d'identité** (`identity`, plus haut) : un rôle est décidé pour l'organisation que la
|
|
355
|
+
couche d'identité résout, jamais pour un claim lu ici ;
|
|
356
|
+
- **un stockage des rôles** : par défaut le fichier JSON nommé par `MCP_ROLES_FILE` (format dans
|
|
357
|
+
`examples/roles.example.json` : les rôles, et `agents` qui lie chaque sujet vérifié à un rôle).
|
|
358
|
+
Un fichier absent, illisible ou mal formé arrête le serveur ; aucun fichier ne veut jamais dire
|
|
359
|
+
« tous les outils ». Un déploiement avec un autre stockage injecte `toolRoles` (`src/toolRoles.ts`,
|
|
360
|
+
`RoleProvider`).
|
|
361
|
+
|
|
362
|
+
L'outil d'identité `whoami` reste hors du contrôle : tout appelant peut lui demander qui il est,
|
|
363
|
+
quel rôle décide de ses outils et quels outils ce rôle accorde (`rolesApplicable: false` sur le
|
|
364
|
+
palier public et en stdio, où il n'y a pas d'appelant). Tests : `tests/toolRoles.test.ts`.
|
|
323
365
|
|
|
324
366
|
## Appeler une API amont (OAuth 2 client_credentials)
|
|
325
367
|
|
|
@@ -522,7 +564,10 @@ ligne.
|
|
|
522
564
|
le client envoie `X-MCP-Locale: fr|en` (défaut `en`) ; le sélecteur navigateur suit
|
|
523
565
|
`Accept-Language`. Les fichiers de ressources (`src/locales/{en,fr}.json`) gardent la
|
|
524
566
|
convention `<Namespace>.<category>.<key>` de `@vantageos/mosaic-i18n`, FR et EN en
|
|
525
|
-
parité.
|
|
567
|
+
parité. Ils sont intégrés au module au build (`scripts/embed-static.mjs` écrit
|
|
568
|
+
`src/embedded.generated.ts`, avec la version et les champs d'identité du paquet) : rien n'est lu sur le disque
|
|
569
|
+
à l'import, et un consommateur empaqueté (le bundler Convex, esbuild) exécute le paquet sans dossier `locales/`
|
|
570
|
+
ni `package.json` à côté ; `--check` refuse une copie périmée.
|
|
526
571
|
|
|
527
572
|
## Un README français généré depuis un README bilingue
|
|
528
573
|
|
|
@@ -553,6 +598,13 @@ pilote l'app Hono en processus avec le `Client` du SDK v2 ; la suite stdio lance
|
|
|
553
598
|
`dist/stdio.js` construit. `npm run evals` exécute le jeu de cas déclaratif contre
|
|
554
599
|
stdio.
|
|
555
600
|
|
|
601
|
+
**Accessibilité (mcp-app-standard A-4).** `npm run test:a11y` audite le widget livré, `dist/ui-bundle/widget.html`
|
|
602
|
+
(construisez-le d'abord avec `npm run build:ui`), selon WCAG 2.1 A et AA avec `axe-core`, en anglais et en français,
|
|
603
|
+
vide et rempli. Toute violation grave ou critique le fait échouer, et un contrôle positif prouve qu'une violation
|
|
604
|
+
injectée est trouvée. Il fait partie de `npm test` : `prepublishOnly` l'exécute avant chaque publication. L'audit
|
|
605
|
+
tourne dans jsdom, qui ne calcule aucune mise en page : le contraste des couleurs y est signalé comme incomplet et
|
|
606
|
+
n'est pas prouvé par cette barrière.
|
|
607
|
+
|
|
556
608
|
## Ajouter un outil
|
|
557
609
|
|
|
558
610
|
Déclarez sa fiche dans `src/toolRecord.ts` (couple d'autorité, verbe, table, et pour
|
|
@@ -562,6 +614,55 @@ une liste/recherche le mécanisme et la pagination), enregistrez-le une fois dan
|
|
|
562
614
|
est alors servi sur les deux transports et les deux ères du protocole.
|
|
563
615
|
`tests/toolRecordComplete.test.ts` le garde honnête.
|
|
564
616
|
|
|
617
|
+
La fiche déclare aussi le `domain` de l'outil (le domaine métier qu'un rôle accorde, par exemple `items`) et son
|
|
618
|
+
`mode`, exactement `read` ou `write`, en accord avec `readOnlyHint` (read <-> `true`). C'est sur eux que se décide le
|
|
619
|
+
rôle d'un agent (mcp-server-standard « Agent tool roles », règle 2). Un outil auquel l'un manque, ou dont le mode
|
|
620
|
+
vaut autre chose, n'est servi à personne : le serveur refuse de démarrer et le nomme
|
|
621
|
+
(`tests/toolRoleDeclarations.test.ts`).
|
|
622
|
+
|
|
623
|
+
## Changelog
|
|
624
|
+
|
|
625
|
+
Une pull request ajoute UN fichier, `changelog.d/<branche>.md` (frontmatter `section:` et une entrée), et ne
|
|
626
|
+
modifie jamais `CHANGELOG.md`, dont le bloc `## [Unreleased]` reste vide. `npm run changelog:check`, inclus dans
|
|
627
|
+
`npm test`, refuse un fragment mal formé ou une entrée écrite sous `## [Unreleased]`. À la publication,
|
|
628
|
+
`npm run changelog:release -- --version <x.y.z>` regroupe les fragments en `## [x.y.z] - <date>`
|
|
629
|
+
(`changelog.d/README.md`, `docs/releasing.md`). Deux pull requests qui ajoutent deux fichiers n'entrent jamais
|
|
630
|
+
en conflit, là où deux modifications du même bloc le faisaient.
|
|
631
|
+
|
|
632
|
+
## Identité du serveur et découverte
|
|
633
|
+
|
|
634
|
+
Ce qu'un hôte ou un annuaire lit du serveur avant et pendant la connexion est dérivé, jamais tapé dans le
|
|
635
|
+
source (`src/serverIdentity.ts`, mcp-server-standard S-49 / S-50) :
|
|
636
|
+
|
|
637
|
+
| Servi comme | Variable d'environnement | Sinon, `package.json` |
|
|
638
|
+
|---|---|---|
|
|
639
|
+
| `serverInfo.title` | `MCP_SERVER_TITLE` | `displayName` |
|
|
640
|
+
| `serverInfo.description` (anglais) | `MCP_SERVER_DESCRIPTION` | `description` |
|
|
641
|
+
| `serverInfo.description` (français, `X-MCP-Locale: fr`) | `MCP_SERVER_DESCRIPTION_FR` | `description_fr` |
|
|
642
|
+
| `serverInfo.websiteUrl` | `MCP_SERVER_WEBSITE_URL` (https, ou http en bouclage) | `homepage` |
|
|
643
|
+
|
|
644
|
+
Un fork modifie ces champs de `package.json`, ou définit les variables (`.env.example`). Une valeur qu'aucune
|
|
645
|
+
des deux sources ne porte arrête le serveur au démarrage, en nommant la variable et le champ : le serveur ne
|
|
646
|
+
sert jamais de valeur fictive.
|
|
647
|
+
|
|
648
|
+
- **Instructions** (`initialize` / `server/discover`) : comment un agent utilise les outils, dans la langue de
|
|
649
|
+
l'appelant, depuis `Mcp.server.instructions` dans `src/locales/{en,fr}.json`. Réécrivez-les pour vos
|
|
650
|
+
outils : `tests/serverMetadata.test.ts` échoue quand un outil servi n'y est pas nommé ou que le texte dépasse
|
|
651
|
+
2048 octets (le budget de champ de Claude Code).
|
|
652
|
+
- **Icône** : un vectoriel, `src/serverIcon.ts` (remplacez le SVG par votre logo), servi à `/icon.svg` et
|
|
653
|
+
nommé dans `serverInfo.icons` ; en stdio il voyage en URI `data:`.
|
|
654
|
+
- **Carte serveur** : la MCP Server Card (forme SEP-2127, plus `capabilities` et un résumé `tools` lu sur les
|
|
655
|
+
outils enregistrés), servie sans identifiant à `/mcp/server-card`, `/.well-known/mcp/server-card.json`,
|
|
656
|
+
`/.well-known/mcp-server-card`, `/mcp.json` et `/.well-known/mcp.json`. Son `name` est en DNS inversé, dérivé
|
|
657
|
+
du site (`io.github.<propriétaire>/<nom>` pour une URL GitHub).
|
|
658
|
+
- **Page d'installation** : un `GET /mcp` de navigateur (sans `Authorization`, avec un `Accept` sans
|
|
659
|
+
`text/event-stream`) reçoit une page bilingue qui nomme l'URL MCP et comment se connecter, sur les deux
|
|
660
|
+
paliers. Toute requête MCP est inchangée, y compris le 401 + `WWW-Authenticate` du palier OAuth.
|
|
661
|
+
|
|
662
|
+
La carte, la page et l'URL de l'icône nomment `PUBLIC_BASE_URL` + `/mcp` quand il est défini. Non défini
|
|
663
|
+
(développement), elles nomment l'origine à laquelle la requête était adressée et partent avec
|
|
664
|
+
`Cache-Control: no-store`. Définissez `PUBLIC_BASE_URL` sur tout déploiement public.
|
|
665
|
+
|
|
565
666
|
## Packaging & distribution
|
|
566
667
|
|
|
567
668
|
Quatre manifestes sont **générés** depuis `scripts/plugin-manifest.source.json` et
|
|
@@ -581,6 +682,34 @@ Claude être surchargé par l'installateur. Le skill d'onboarding est
|
|
|
581
682
|
`skills/connect-mcp-server/SKILL.md`, déclaré pour OpenAI en
|
|
582
683
|
`extensions["com.openai"].onboardingSkill`.
|
|
583
684
|
|
|
685
|
+
### Hôtes cibles et chemins de vérification
|
|
686
|
+
|
|
687
|
+
`mcp-server.json` déclare où le serveur est distribué, sous `distribution.targets` (par défaut : `claude`,
|
|
688
|
+
`claude-code`, `vscode`, `cursor`, `chatgpt`), comme le demande la règle S-45 du mcp-server-standard. Un hôte qui
|
|
689
|
+
vérifie que vous possédez l'origine en lisant un chemin connu voit ce chemin servi pour chaque cible déclarée, la
|
|
690
|
+
valeur étant lue dans l'environnement à chaque requête, jamais écrite dans le code :
|
|
691
|
+
|
|
692
|
+
| Cible | Chemin | Variable |
|
|
693
|
+
|---|---|---|
|
|
694
|
+
| `chatgpt` | `/.well-known/openai-apps-challenge` | `OPENAI_APPS_CHALLENGE` |
|
|
695
|
+
| `google-connected-apps` | `/.well-known/google-connected-apps` | `GOOGLE_CONNECTED_APPS_VERIFICATION` |
|
|
696
|
+
|
|
697
|
+
Non définie, la variable fait répondre le chemin en 404 en la nommant : la vérification de l'hôte échoue de façon
|
|
698
|
+
visible. Un fork retire les cibles qu'il ne vise pas, définit les variables de celles qu'il garde, et règle
|
|
699
|
+
`PUBLIC_BASE_URL` sur son origine publique `https://` : la valeur du boilerplate est une valeur de développement, pas
|
|
700
|
+
une origine que vous possédez.
|
|
701
|
+
|
|
702
|
+
### Kit de référencement pour les annuaires
|
|
703
|
+
|
|
704
|
+
`listing/listing.json` est ce que lit le relecteur d'un annuaire (mcp-standard C-35) : la description, chaque outil
|
|
705
|
+
servi avec son titre et ses indications lecture seule / destructif, 5 invites de test positives et 3 négatives
|
|
706
|
+
données par identifiant d'éval de `evals/evals.json`, et la façon d'obtenir un compte de relecture (jamais un compte).
|
|
707
|
+
Chaque champ est dérivé : `scripts/listing-kit.mjs` lit la description et les identifiants d'éval choisis dans
|
|
708
|
+
`scripts/plugin-manifest.source.json` (`listing`), et les outils dans le `tools/list` servi. Après avoir modifié une
|
|
709
|
+
source ou un outil, lancez `npm run listing:write` et commitez le fichier. `npm run build` se contente de le
|
|
710
|
+
vérifier : un kit commité qui s'écarte de ses sources, un identifiant d'éval inconnu ou un compte insuffisant fait
|
|
711
|
+
échouer la construction, et `tests/listingKit.test.ts` échoue aussi. Modifiez la source, jamais le fichier.
|
|
712
|
+
|
|
584
713
|
### Scaffold → build → publish
|
|
585
714
|
|
|
586
715
|
```bash
|
|
@@ -640,9 +769,13 @@ curl -s -H 'Cache-Control: no-cache' https://registry.npmjs.org/@vantageos%2Fmcp
|
|
|
640
769
|
|
|
641
770
|
Sommes égales : le registre sert ce que la source construit. Un serveur en marche
|
|
642
771
|
prouve de la même façon ce qu'il sert : `GET /health` renvoie
|
|
643
|
-
`{ status, version, buildId }` avant toute authentification. `buildId` est
|
|
644
|
-
|
|
645
|
-
`
|
|
772
|
+
`{ status, version, buildId, buildHash }` avant toute authentification. `buildId` est le
|
|
773
|
+
commit git à partir duquel l'arbre servi a été construit, dérivé par `npm run build`
|
|
774
|
+
(`scripts/write-build-info.mjs` écrit `dist/build-info.json`) : `BUILD_COMMIT`, sinon le commit qu'un
|
|
775
|
+
hébergeur injecte au build (`RAILWAY_GIT_COMMIT_SHA`, `VERCEL_GIT_COMMIT_SHA`, `RENDER_GIT_COMMIT`,
|
|
776
|
+
`GITHUB_SHA`, `SOURCE_VERSION`), sinon `git rev-parse HEAD` ; sans aucun, le build échoue en nommant
|
|
777
|
+
`BUILD_COMMIT`. `buildHash` est un hachage des fichiers dont le processus s'exécute ; il change aussi
|
|
778
|
+
quand un arbre modifié ou un `dist/` retouché est servi, ce que ni `version` ni le commit ne garantissent.
|
|
646
779
|
|
|
647
780
|
## Couche UI MCP Apps
|
|
648
781
|
|
|
@@ -658,7 +791,10 @@ n'y a pas d'outil de rendu séparé.
|
|
|
658
791
|
- La ressource déclare `_meta.ui` : `csp` avec `connectDomains`, `resourceDomains`,
|
|
659
792
|
`frameDomains` et `baseUriDomains` (tous vides — le widget ne fait aucune requête
|
|
660
793
|
réseau, ne charge aucun actif tiers et n'intègre aucune frame), `prefersBorder`, et
|
|
661
|
-
`domain`
|
|
794
|
+
`domain` : l'origine de bac à sable de Claude, dérivée au démarrage de l'URL MCP servie
|
|
795
|
+
(`PUBLIC_BASE_URL` + `/mcp`) comme les 32 premiers caractères hexadécimaux de son SHA-256 suivis de
|
|
796
|
+
`.claudemcpcontent.com` (mcp-app-standard A-11), jamais tapée. Sans `PUBLIC_BASE_URL`, ou en stdio, il
|
|
797
|
+
n'y a pas d'URL servie et aucun domaine n'est déclaré.
|
|
662
798
|
Son type MIME est `text/html;profile=mcp-app`.
|
|
663
799
|
- **Les clés ChatGPT sont additives** : `openai/outputTemplate` (même URI) et les
|
|
664
800
|
chaînes `openai/toolInvocation/invoking|invoked`. Rien ne les lit ; le serveur
|
package/README.md
CHANGED
|
@@ -22,7 +22,10 @@ src/
|
|
|
22
22
|
toolRecord.ts # typed tool record: authority, verb, table, mechanism, pagination, cost
|
|
23
23
|
pagination.ts # cursor pagination applied by the tool (default, finite max, invalid_cursor)
|
|
24
24
|
signedCursor.ts # HMAC-signed cursor primitive (createCursorSigner), used by every cursor
|
|
25
|
-
buildIdentity.ts # build identity
|
|
25
|
+
buildIdentity.ts # build identity: the git commit built (buildId) + the hash of the built files (buildHash)
|
|
26
|
+
serverIdentity.ts # title, description, website, icon, instructions: package.json or env
|
|
27
|
+
serverCard.ts # the public MCP Server Card
|
|
28
|
+
landingPage.ts # the setup page a browser gets at /mcp
|
|
26
29
|
stdio.ts # serveStdio(factory): both protocol eras over stdio
|
|
27
30
|
http.ts # Node http.Server adapter around the Hono app
|
|
28
31
|
httpApp.ts # createMcpHandler(factory) on /mcp + the OAuth gate
|
|
@@ -110,6 +113,12 @@ stays with the protocol list methods' cursor, which the SDK serves.
|
|
|
110
113
|
fail on any gap. Annotations are untrusted hints for the client (mcp-standard C-15);
|
|
111
114
|
never gate an action on them alone.
|
|
112
115
|
|
|
116
|
+
Every tool invocation is rate limited (mcp-server-standard S-24): a token bucket per caller (the resolved
|
|
117
|
+
organisation, or `public` on the unscoped tier), shared by the process. `TOOL_RATE_LIMIT_BURST` calls may burst
|
|
118
|
+
(default 30), then the bucket refills at `TOOL_RATE_LIMIT_PER_MINUTE` (default 60). A call past the limit is a tool
|
|
119
|
+
error with `structuredContent.error.code` `rate_limited` and `retryAfterMs` (message FR/EN); a limit that is not a
|
|
120
|
+
positive integer stops the server at start. A new tool wraps its handler with `withRateLimit(…)`.
|
|
121
|
+
|
|
113
122
|
## Authentication tiers
|
|
114
123
|
|
|
115
124
|
| `AUTH_MODE` | Behavior |
|
|
@@ -163,7 +172,11 @@ many clients. To measure it, send 21 registrations from one client and expect a
|
|
|
163
172
|
the 21st. An IPv6 client is keyed on its /64 and ALSO counted against its /48: every /64 inside one
|
|
164
173
|
/48 shares `MCP_REGISTER_LIMIT_PER_48_PER_MINUTE` registrations per minute (default `100`), so
|
|
165
174
|
one /48, which holds 65536 /64s, cannot fill the client registry to its `503`. An invalid value
|
|
166
|
-
stops the server at start.
|
|
175
|
+
stops the server at start. `/token` is limited the same way, keyed on the same address:
|
|
176
|
+
`MCP_TOKEN_LIMIT_PER_MINUTE` requests per minute (default `60`), five times that per IPv6 /48, then `429` with
|
|
177
|
+
`Retry-After: 60`. Each `private_key_jwt` client assertion it verifies holds a single-use slot until it expires
|
|
178
|
+
(at most 300 s, the identity layer's cap), so an unlimited caller could fill those slots and lock every
|
|
179
|
+
assertion-authenticated client out; the limit bounds that.
|
|
167
180
|
|
|
168
181
|
**Request limits and CORS**: An operator variable that is set but invalid (for example `MCP_MAX_BODY_BYTES=abc`) stops the server at startup with the variable's name and the expected form, never a silent default; unset or empty keeps the default. the Node entry reads at most `MCP_MAX_BODY_BYTES`
|
|
169
182
|
(default 1 MiB) of a request body and answers `413` before any handler runs.
|
|
@@ -299,10 +312,37 @@ server.registerTool("my_rows", config, withVerifiedPerson(deps, async (args, per
|
|
|
299
312
|
|
|
300
313
|
Unless the person resolved, the handler does not run: the call gets an MCP tool error with the
|
|
301
314
|
stable code `person_unresolved` (`structuredContent.error.code`), a FR/EN message per
|
|
302
|
-
`X-MCP-Locale`, and a `reason`: `identity_not_configured` (no `identity` injected
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
315
|
+
`X-MCP-Locale`, and a `reason`: `identity_not_configured` (no `identity` injected — on the public
|
|
316
|
+
tier only, since the OAuth tier refuses to start without it, see below), `no_verified_session`
|
|
317
|
+
(public tier, and **stdio, which never has a person**: there is no bearer on that transport) or
|
|
318
|
+
`refused` (the package refused; its `code` and `reason` are carried, never a claim value). On the
|
|
319
|
+
public tier and stdio, a tool that does not wrap its handler runs with no person. On the OAuth
|
|
320
|
+
tier no tool runs for a caller the identity layer did not resolve: the role gate below answers
|
|
321
|
+
first, with this same refusal.
|
|
322
|
+
|
|
323
|
+
### Agent tool roles
|
|
324
|
+
|
|
325
|
+
On the OAuth tier, every tool is granted or refused by the caller's ROLE, through
|
|
326
|
+
`@vantageos/cloud-identity`'s `decideToolRole` (mcp-server-standard "Agent tool roles"). Each tool
|
|
327
|
+
declares its `domain` and `mode` (see "Adding a tool"); a role grants read and/or write per domain,
|
|
328
|
+
plus `add` (tools granted outside its domains) and `remove` (tools withheld, which always wins).
|
|
329
|
+
The same decision drives both points: `tools/list` holds only the granted tools, and a call to any
|
|
330
|
+
other tool is a tool result with `isError: true` and `structuredContent.error.code`
|
|
331
|
+
`TOOL_ROLE_DENIED` (`TOOL_ROLE_UNRESOLVED` when the role store could not be read), its handler
|
|
332
|
+
never run.
|
|
333
|
+
|
|
334
|
+
The start is refused, naming each missing piece, unless both of these are present:
|
|
335
|
+
|
|
336
|
+
- **the identity stores** (`identity`, above): a role is decided for the organisation the identity
|
|
337
|
+
layer resolves, never for a claim read here;
|
|
338
|
+
- **a role store**: by default the JSON file named by `MCP_ROLES_FILE` (format in
|
|
339
|
+
`examples/roles.example.json`: the roles, and `agents` binding each verified subject to one role).
|
|
340
|
+
A missing, unreadable or malformed file stops the server; no file never means "every tool". A
|
|
341
|
+
deployment with another store injects `toolRoles` (`src/toolRoles.ts`, `RoleProvider`).
|
|
342
|
+
|
|
343
|
+
The identity tool `whoami` stays outside the gate: every caller can ask it who they are, which role
|
|
344
|
+
decides their tools, and which tools that role grants (`rolesApplicable: false` on the public tier
|
|
345
|
+
and stdio, where there is no caller). Tests: `tests/toolRoles.test.ts`.
|
|
306
346
|
|
|
307
347
|
## Calling an upstream API (OAuth 2 client_credentials)
|
|
308
348
|
|
|
@@ -494,7 +534,10 @@ row value.
|
|
|
494
534
|
client sends `X-MCP-Locale: fr|en` (default `en`); the browser picker follows
|
|
495
535
|
`Accept-Language`. Resource files (`src/locales/{en,fr}.json`) keep the
|
|
496
536
|
`<Namespace>.<category>.<key>` convention of `@vantageos/mosaic-i18n`, FR and EN
|
|
497
|
-
in lockstep.
|
|
537
|
+
in lockstep. They are embedded in the module at build (`scripts/embed-static.mjs` writes
|
|
538
|
+
`src/embedded.generated.ts`, with the package version and identity fields), so nothing is read from disk at
|
|
539
|
+
import and a bundled consumer (the Convex bundler, esbuild) runs the package with no `locales/` folder or
|
|
540
|
+
`package.json` beside it; `--check` refuses a stale copy.
|
|
498
541
|
|
|
499
542
|
## A French README generated from a bilingual one
|
|
500
543
|
|
|
@@ -524,6 +567,12 @@ Runs both transports through the real SDK code paths: the HTTP suite drives the
|
|
|
524
567
|
Hono app in-process with the SDK v2 `Client`; the stdio suite spawns the built
|
|
525
568
|
`dist/stdio.js`. `npm run evals` runs the declarative case-set against stdio.
|
|
526
569
|
|
|
570
|
+
**Accessibility (mcp-app-standard A-4).** `npm run test:a11y` audits the shipped widget, `dist/ui-bundle/widget.html`
|
|
571
|
+
(build it first with `npm run build:ui`), against WCAG 2.1 A and AA with `axe-core`, in English and French, empty and
|
|
572
|
+
populated. Any serious or critical violation fails it, and a positive control proves an injected violation is found.
|
|
573
|
+
It is part of `npm test`, so `prepublishOnly` runs it before every publish. The audit runs in jsdom, which computes no
|
|
574
|
+
layout, so colour contrast is reported as incomplete and is not proven by this gate.
|
|
575
|
+
|
|
527
576
|
## Adding a tool
|
|
528
577
|
|
|
529
578
|
Declare its record in `src/toolRecord.ts` (authority pair, verb, table, and for a
|
|
@@ -532,6 +581,53 @@ list/search the mechanism and pagination), register it once in
|
|
|
532
581
|
`inputSchema` and `outputSchema`, and return `structuredResult(...)`. It is then served on both transports and both
|
|
533
582
|
protocol eras. `tests/toolRecordComplete.test.ts` keeps it honest.
|
|
534
583
|
|
|
584
|
+
The record also declares the tool's `domain` (the business area a role grants, such as `items`) and its `mode`,
|
|
585
|
+
exactly `read` or `write`, agreeing with `readOnlyHint` (read <-> `true`). This is what an agent's role is decided
|
|
586
|
+
on (mcp-server-standard "Agent tool roles", rule 2). A tool missing either, or with any other mode, is served to no
|
|
587
|
+
one: the server refuses to start and names it (`tests/toolRoleDeclarations.test.ts`).
|
|
588
|
+
|
|
589
|
+
## Changelog
|
|
590
|
+
|
|
591
|
+
A pull request adds ONE file, `changelog.d/<branch>.md` (frontmatter `section:` plus one entry), and never
|
|
592
|
+
edits `CHANGELOG.md`, whose `## [Unreleased]` block stays empty. `npm run changelog:check`, part of `npm test`,
|
|
593
|
+
refuses a malformed fragment or an entry written under `## [Unreleased]`. At release,
|
|
594
|
+
`npm run changelog:release -- --version <x.y.z>` folds the fragments into `## [x.y.z] - <date>`
|
|
595
|
+
(`changelog.d/README.md`, `docs/releasing.md`). Two pull requests adding two files never conflict, where two
|
|
596
|
+
edits of the same block did.
|
|
597
|
+
|
|
598
|
+
## Server identity and discovery
|
|
599
|
+
|
|
600
|
+
What a host or a directory reads about the server before and at connection is derived, never typed in
|
|
601
|
+
source (`src/serverIdentity.ts`, mcp-server-standard S-49 / S-50):
|
|
602
|
+
|
|
603
|
+
| Served as | Environment override | Otherwise, `package.json` |
|
|
604
|
+
|---|---|---|
|
|
605
|
+
| `serverInfo.title` | `MCP_SERVER_TITLE` | `displayName` |
|
|
606
|
+
| `serverInfo.description` (English) | `MCP_SERVER_DESCRIPTION` | `description` |
|
|
607
|
+
| `serverInfo.description` (French, `X-MCP-Locale: fr`) | `MCP_SERVER_DESCRIPTION_FR` | `description_fr` |
|
|
608
|
+
| `serverInfo.websiteUrl` | `MCP_SERVER_WEBSITE_URL` (https, or http on loopback) | `homepage` |
|
|
609
|
+
|
|
610
|
+
A fork edits those `package.json` fields, or sets the variables (`.env.example`). A value neither source
|
|
611
|
+
carries stops the server at start, naming the variable and the field: the server never serves a placeholder.
|
|
612
|
+
|
|
613
|
+
- **Instructions** (`initialize` / `server/discover`): how an agent uses the tools, in the caller's locale, from
|
|
614
|
+
`Mcp.server.instructions` in `src/locales/{en,fr}.json`. Rewrite them for your tools:
|
|
615
|
+
`tests/serverMetadata.test.ts` fails when a served tool is not named or the text passes 2048 bytes (Claude
|
|
616
|
+
Code's field budget).
|
|
617
|
+
- **Icon**: a vector, `src/serverIcon.ts` (replace the SVG with your mark), served at `/icon.svg` and named
|
|
618
|
+
in `serverInfo.icons`; on stdio it travels as a `data:` URI.
|
|
619
|
+
- **Server card**: the MCP Server Card (SEP-2127 shape, plus `capabilities` and a `tools` summary read from
|
|
620
|
+
the registered tools), served without credential at `/mcp/server-card`, `/.well-known/mcp/server-card.json`,
|
|
621
|
+
`/.well-known/mcp-server-card`, `/mcp.json` and `/.well-known/mcp.json`. Its `name` is reverse-DNS, derived
|
|
622
|
+
from the website (`io.github.<owner>/<name>` for a GitHub URL).
|
|
623
|
+
- **Setup page**: a browser `GET /mcp` (no `Authorization`, an `Accept` without `text/event-stream`) gets a
|
|
624
|
+
bilingual page naming the MCP URL and how to connect, on both tiers. Every MCP request is unchanged,
|
|
625
|
+
including the OAuth tier's 401 + `WWW-Authenticate`.
|
|
626
|
+
|
|
627
|
+
The card, the page and the icon URL name `PUBLIC_BASE_URL` + `/mcp` when it is set. Unset (development),
|
|
628
|
+
they name the origin the request was addressed to and are sent `Cache-Control: no-store`. Set
|
|
629
|
+
`PUBLIC_BASE_URL` on every public deployment.
|
|
630
|
+
|
|
535
631
|
## Packaging & distribution
|
|
536
632
|
|
|
537
633
|
Four manifests are **generated** from `scripts/plugin-manifest.source.json` and
|
|
@@ -551,6 +647,33 @@ to be overridden by the installer. The onboarding skill is
|
|
|
551
647
|
`skills/connect-mcp-server/SKILL.md`, declared for OpenAI as
|
|
552
648
|
`extensions["com.openai"].onboardingSkill`.
|
|
553
649
|
|
|
650
|
+
### Target hosts and their verification paths
|
|
651
|
+
|
|
652
|
+
`mcp-server.json` declares where the server is distributed, under `distribution.targets` (default: `claude`,
|
|
653
|
+
`claude-code`, `vscode`, `cursor`, `chatgpt`), as mcp-server-standard S-45 asks. A host that confirms you own the
|
|
654
|
+
origin by reading a well-known path gets that path served for each declared target, with the value read from the
|
|
655
|
+
environment at request time, never written in source:
|
|
656
|
+
|
|
657
|
+
| Target | Path | Variable |
|
|
658
|
+
|---|---|---|
|
|
659
|
+
| `chatgpt` | `/.well-known/openai-apps-challenge` | `OPENAI_APPS_CHALLENGE` |
|
|
660
|
+
| `google-connected-apps` | `/.well-known/google-connected-apps` | `GOOGLE_CONNECTED_APPS_VERIFICATION` |
|
|
661
|
+
|
|
662
|
+
Unset, the path answers 404 naming the variable, so the host's check fails visibly. A fork removes the targets it
|
|
663
|
+
does not ship to, sets the variables of the ones it keeps, and sets `PUBLIC_BASE_URL` to its public `https://` origin:
|
|
664
|
+
the boilerplate's own value is a development default, not an origin you own.
|
|
665
|
+
|
|
666
|
+
### Directory listing kit
|
|
667
|
+
|
|
668
|
+
`listing/listing.json` is what a directory reviewer reads (mcp-standard C-35): the description, every served tool
|
|
669
|
+
with its title and its read-only / destructive hints, 5 positive and 3 negative test prompts given as eval ids of
|
|
670
|
+
`evals/evals.json`, and where a reviewer obtains an account (never one). Every field is derived:
|
|
671
|
+
`scripts/listing-kit.mjs` reads the description and the chosen eval ids from
|
|
672
|
+
`scripts/plugin-manifest.source.json` (`listing`), and the tools from the served `tools/list`. After changing a
|
|
673
|
+
source or a tool, run `npm run listing:write` and commit the file. `npm run build` only checks it: a committed kit
|
|
674
|
+
that differs from its sources, an unknown eval id or a short count fails the build, and `tests/listingKit.test.ts`
|
|
675
|
+
fails too. Edit the source, never the file.
|
|
676
|
+
|
|
554
677
|
### Scaffold → build → publish
|
|
555
678
|
|
|
556
679
|
```bash
|
|
@@ -606,9 +729,13 @@ curl -s -H 'Cache-Control: no-cache' https://registry.npmjs.org/@vantageos%2Fmcp
|
|
|
606
729
|
```
|
|
607
730
|
|
|
608
731
|
Equal sums: the registry serves what the source builds. A running server proves what
|
|
609
|
-
it serves the same way: `GET /health` returns `{ status, version, buildId }` before any
|
|
610
|
-
authentication. `buildId` is
|
|
611
|
-
|
|
732
|
+
it serves the same way: `GET /health` returns `{ status, version, buildId, buildHash }` before any
|
|
733
|
+
authentication. `buildId` is the git commit the served tree was built from, derived by `npm run build`
|
|
734
|
+
(`scripts/write-build-info.mjs` writes `dist/build-info.json`): `BUILD_COMMIT`, else the commit a host
|
|
735
|
+
injects at build time (`RAILWAY_GIT_COMMIT_SHA`, `VERCEL_GIT_COMMIT_SHA`, `RENDER_GIT_COMMIT`, `GITHUB_SHA`,
|
|
736
|
+
`SOURCE_VERSION`), else `git rev-parse HEAD`; with none, the build fails, naming `BUILD_COMMIT`. `buildHash` is
|
|
737
|
+
a hash of the files the process runs from, so it also changes when a dirty tree or a patched `dist/` is
|
|
738
|
+
served, which neither `version` nor the commit guarantees.
|
|
612
739
|
|
|
613
740
|
## MCP Apps UI layer
|
|
614
741
|
|
|
@@ -624,8 +751,10 @@ separate render tool.
|
|
|
624
751
|
- The resource declares `_meta.ui`: `csp` with `connectDomains`,
|
|
625
752
|
`resourceDomains`, `frameDomains` and `baseUriDomains` (all empty — the widget
|
|
626
753
|
makes no network request, loads no third-party asset and embeds no frame),
|
|
627
|
-
`prefersBorder`, and `domain
|
|
628
|
-
|
|
754
|
+
`prefersBorder`, and `domain`: Claude's sandbox origin, derived at start from the served MCP URL
|
|
755
|
+
(`PUBLIC_BASE_URL` + `/mcp`) as the first 32 hex characters of its SHA-256 followed by
|
|
756
|
+
`.claudemcpcontent.com` (mcp-app-standard A-11), never typed. Without `PUBLIC_BASE_URL`, or on stdio,
|
|
757
|
+
there is no served URL and no domain is declared. Its mime type is `text/html;profile=mcp-app`.
|
|
629
758
|
- **ChatGPT keys are additive**: `openai/outputTemplate` (same URI) and the
|
|
630
759
|
`openai/toolInvocation/invoking|invoked` strings. Nothing reads them; the
|
|
631
760
|
server works with every `openai/*` key absent. `openai/ui` entrypoints are
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"commit":"4193f2a535d8229f071524265a7e6bb98e19d34e"}
|
package/dist/buildIdentity.d.ts
CHANGED
|
@@ -17,3 +17,19 @@
|
|
|
17
17
|
export declare function computeBuildIdentity(dir: string): string;
|
|
18
18
|
/** The identity of the tree THIS process runs from, computed once. */
|
|
19
19
|
export declare function getBuildIdentity(): string;
|
|
20
|
+
/**
|
|
21
|
+
* The git commit the served tree was built from: the `buildId` of /health and serverInfo (C-25,
|
|
22
|
+
* ruling: build identity = the commit). It is DERIVED at build time by
|
|
23
|
+
* scripts/write-build-info.mjs (`npm run build`), which writes `dist/build-info.json`, and read
|
|
24
|
+
* here — never typed. `src/` and `dist/` both sit one level under the package root, so the same
|
|
25
|
+
* relative URL resolves in source mode (vitest) and in the built output.
|
|
26
|
+
*
|
|
27
|
+
* The hash of the built files above stays served beside it as `buildHash`: the commit names the
|
|
28
|
+
* source, the hash proves the files a process runs from (a dirty tree or a patched dist changes
|
|
29
|
+
* the hash, not the commit).
|
|
30
|
+
*/
|
|
31
|
+
export declare const BUILD_INFO_URL: URL;
|
|
32
|
+
/** The commit recorded in a build-info file. Throws, naming the file, if it is not readable. */
|
|
33
|
+
export declare function readBuildCommit(path: string): string;
|
|
34
|
+
/** The build commit of THIS process, read once. */
|
|
35
|
+
export declare function getBuildCommit(): string;
|
package/dist/buildIdentity.js
CHANGED
|
@@ -60,3 +60,42 @@ export function getBuildIdentity() {
|
|
|
60
60
|
processIdentity ??= computeBuildIdentity(dirname(fileURLToPath(import.meta.url)));
|
|
61
61
|
return processIdentity;
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* The git commit the served tree was built from: the `buildId` of /health and serverInfo (C-25,
|
|
65
|
+
* ruling: build identity = the commit). It is DERIVED at build time by
|
|
66
|
+
* scripts/write-build-info.mjs (`npm run build`), which writes `dist/build-info.json`, and read
|
|
67
|
+
* here — never typed. `src/` and `dist/` both sit one level under the package root, so the same
|
|
68
|
+
* relative URL resolves in source mode (vitest) and in the built output.
|
|
69
|
+
*
|
|
70
|
+
* The hash of the built files above stays served beside it as `buildHash`: the commit names the
|
|
71
|
+
* source, the hash proves the files a process runs from (a dirty tree or a patched dist changes
|
|
72
|
+
* the hash, not the commit).
|
|
73
|
+
*/
|
|
74
|
+
export const BUILD_INFO_URL = new URL("../dist/build-info.json", import.meta.url);
|
|
75
|
+
const FULL_COMMIT = /^[0-9a-f]{40}$/;
|
|
76
|
+
/** The commit recorded in a build-info file. Throws, naming the file, if it is not readable. */
|
|
77
|
+
export function readBuildCommit(path) {
|
|
78
|
+
let commit;
|
|
79
|
+
try {
|
|
80
|
+
commit = JSON.parse(readFileSync(path, "utf8")).commit;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
throw new Error(tBoth("buildIdentity.commitUnreadable", {
|
|
84
|
+
file: path,
|
|
85
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
if (typeof commit !== "string" || !FULL_COMMIT.test(commit)) {
|
|
89
|
+
throw new Error(tBoth("buildIdentity.commitUnreadable", {
|
|
90
|
+
file: path,
|
|
91
|
+
reason: "commit is not a 40-hex git commit id",
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
return commit;
|
|
95
|
+
}
|
|
96
|
+
let processCommit;
|
|
97
|
+
/** The build commit of THIS process, read once. */
|
|
98
|
+
export function getBuildCommit() {
|
|
99
|
+
processCommit ??= readBuildCommit(fileURLToPath(BUILD_INFO_URL));
|
|
100
|
+
return processCommit;
|
|
101
|
+
}
|