@vantageos/mcp-boilerplate 0.2.3 → 0.3.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 +161 -0
- package/README.fr.md +323 -15
- package/README.md +306 -15
- package/dist/buildIdentity.d.ts +19 -0
- package/dist/buildIdentity.js +62 -0
- package/dist/declaredVariables.d.ts +205 -0
- package/dist/declaredVariables.js +451 -0
- package/dist/exampleItems.d.ts +15 -0
- package/dist/exampleItems.js +28 -1
- package/dist/httpApp.d.ts +17 -0
- package/dist/httpApp.js +19 -2
- package/dist/i18n.d.ts +10 -0
- package/dist/i18n.js +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +6 -0
- package/dist/lake/convex.d.ts +101 -0
- package/dist/lake/convex.js +224 -0
- package/dist/lake/reader.d.ts +88 -0
- package/dist/lake/reader.js +120 -0
- package/dist/locales/en.json +42 -0
- package/dist/locales/fr.json +42 -0
- package/dist/mcpServerFactory.d.ts +31 -14
- package/dist/mcpServerFactory.js +116 -48
- package/dist/oauth/authorizationServer.js +64 -18
- package/dist/oauth/clerk.d.ts +2 -0
- package/dist/oauth/clerk.js +15 -5
- package/dist/oauth/clientRegistry.d.ts +19 -4
- package/dist/oauth/clientRegistry.js +20 -10
- package/dist/oauth/picker.d.ts +7 -2
- package/dist/oauth/picker.js +90 -13
- package/dist/oauth/registerLimiter.d.ts +34 -0
- package/dist/oauth/registerLimiter.js +95 -0
- package/dist/oauth/settings.d.ts +4 -0
- package/dist/oauth/settings.js +4 -0
- package/dist/pagination.d.ts +66 -0
- package/dist/pagination.js +100 -0
- package/dist/serverManifest.d.ts +32 -0
- package/dist/serverManifest.js +68 -0
- package/dist/stdio.js +5 -1
- package/dist/tenancy.d.ts +1 -1
- package/dist/toolRecord.d.ts +94 -0
- package/dist/toolRecord.js +52 -0
- package/dist/ui/uiResource.d.ts +11 -4
- package/dist/ui/uiResource.js +8 -4
- package/dist/upstream/clientCredentials.d.ts +106 -0
- package/dist/upstream/clientCredentials.js +283 -0
- package/dist/upstream/paginatedClient.d.ts +142 -0
- package/dist/upstream/paginatedClient.js +422 -0
- package/dist/verifiedPerson.d.ts +85 -0
- package/dist/verifiedPerson.js +112 -0
- package/docs/oauth-stores.md +2 -1
- package/examples/oauth.env.example +7 -0
- package/mcp-server.json +13 -0
- package/package.json +19 -4
- package/plugin.json +1 -1
- package/skills/connect-mcp-server/SKILL.md +5 -5
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vantage-mcp-boilerplate",
|
|
3
3
|
"displayName": "VantageOS MCP Boilerplate",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.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,167 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-10-28
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Data-lake reader** (`src/lake/reader.ts`, exported from the package root): a derived server reads
|
|
15
|
+
its client's data lake through a READ-ONLY interface, `LakeReader` — `listPage({ after?, limit,
|
|
16
|
+
scope? })` -> `{ rows, exhausted }` in ascending key order, `get(key, scope?)`, `keyOf(row)`; no
|
|
17
|
+
write method. `list_items` reads through the reader injected as `AppDeps.lake` /
|
|
18
|
+
`McpServerDeps.lake`; the default is the in-memory seed (`EXAMPLE_ITEMS_READER`,
|
|
19
|
+
`createInMemoryLakeReader`), so the output is unchanged. The caller's tenant travels into the read
|
|
20
|
+
as `scope`, and a row of another tenant coming back fails the call closed. `readPage` (exported)
|
|
21
|
+
applies the tool's bounds and cursor to any reader and checks each page against the contract: an
|
|
22
|
+
unreadable page is a `LakeReadError` (`malformed_page`, `malformed_row`, `out_of_scope`,
|
|
23
|
+
`read_failed`), answered by `list_items` as a tool error with the stable code `lake_unreadable`
|
|
24
|
+
and a FR/EN message, never an empty page. The `list_items` widget resource no longer takes rows it
|
|
25
|
+
never rendered (`buildUiResource({ domain })`).
|
|
26
|
+
- **Convex lake adapter** at its own subpath, `@vantageos/mcp-boilerplate/lake/convex`, with `convex`
|
|
27
|
+
as an OPTIONAL peer dependency (`^1.46.0`; dev dependency pinned `1.46.0`).
|
|
28
|
+
`createConvexLakeReader` takes the deployment URL (https, or http on loopback; no query, fragment
|
|
29
|
+
or credentials), an optional read token (sent through `setAuth`, or as the argument named by
|
|
30
|
+
`tokenArg`), the NAMES of the page and get query functions and the key field — all configuration
|
|
31
|
+
— and an optional `parseRow`. It only ever calls `ConvexHttpClient.query`, never a mutation, an
|
|
32
|
+
action or `function`. `convexLakeReaderFromEnv(env, names, options)` declares the connection
|
|
33
|
+
variables with `defineVariables` (url; token as a secret) and refuses at start, naming every
|
|
34
|
+
missing or invalid variable, never a value. A failing call is `read_failed` and the client's
|
|
35
|
+
message is not forwarded; no error carries the token, the URL or a row value.
|
|
36
|
+
- **The verified person reaches tool handlers** (`src/verifiedPerson.ts`, exported from the package
|
|
37
|
+
root). With `AppDeps.identity` (`orgLookups`, `principalLookups`, and `adminRoles`, required and
|
|
38
|
+
non-empty, refused at build otherwise, no default role), each `/mcp` request resolves the
|
|
39
|
+
caller's person ONCE through `@vantageos/cloud-identity`'s `resolveCallerFromClaims`, from the
|
|
40
|
+
same verified claims as the tenant; nothing reads a claim locally. The person travels in the
|
|
41
|
+
per-request server deps beside the tenant (`McpServerDeps.caller`), frozen, exactly the package's
|
|
42
|
+
fields (`subject`, `actorName`, `org`, `principal`, `role`, `isOrgAdmin`), a copy that never
|
|
43
|
+
freezes the injected store's objects. A tool declares it needs the person with
|
|
44
|
+
`withVerifiedPerson(deps, handler)`; unless the person resolved, the handler does not run and the
|
|
45
|
+
call is an MCP tool error with the stable code `person_unresolved`, a FR/EN message, and a
|
|
46
|
+
`reason`: `identity_not_configured`, `no_verified_session` (public tier, and stdio, which never
|
|
47
|
+
has a person) or `refused` (the package's refusal `code` and `reason`, never a claim value).
|
|
48
|
+
Tools that do not wrap their handler are unchanged.
|
|
49
|
+
- **Paginated upstream client** (`src/upstream/paginatedClient.ts`, exported from the package root)
|
|
50
|
+
for a derived server that reads a third-party endpoint paginated by offset and limit.
|
|
51
|
+
`createPaginatedUpstreamClient` takes the offset and limit parameter names, their placement
|
|
52
|
+
(`query` or `jsonBody`) and the method as configuration, a REQUIRED `allowedOrigins` list (every
|
|
53
|
+
request URL is checked before any network call, https or loopback http; redirects are refused,
|
|
54
|
+
never followed) and a REQUIRED finite `rowCeiling`. `readAll` walks the pages to the short page
|
|
55
|
+
(or the declared total) and refuses a set above the ceiling with an `UpstreamPageError`
|
|
56
|
+
(`row_ceiling_exceeded`) naming it, never a silently truncated set. `readPage` reads one page per
|
|
57
|
+
MCP call behind an opaque cursor bound to the path and params; a cursor replayed against another
|
|
58
|
+
query or altered without recomputing its unkeyed fingerprint is refused as `invalid_cursor`
|
|
59
|
+
(misuse detection, not forgery protection: a minted offset above the ceiling is refused before any
|
|
60
|
+
request). An empty page before a declared total is `inconsistent_page`, so a walk never
|
|
61
|
+
re-requests the same offset. 429 and 503 are retried with exponential backoff and jitter,
|
|
62
|
+
honouring `Retry-After` (capped); no other status is retried. An unreadable page fails closed.
|
|
63
|
+
Errors name the path and status, never a token, a parameter value or the body. Accepts the token
|
|
64
|
+
source's `fetchWithToken` as `fetchImpl`.
|
|
65
|
+
- **Declared variables: a configuration extension point** (`src/declaredVariables.ts`, exported
|
|
66
|
+
from the package root) for a derived server's own environment variables. `defineVariables`
|
|
67
|
+
takes, per logical key, `{ env, type, required?, default?, secret?, description: { en, fr } }`
|
|
68
|
+
with `type` one of `string`, `integer` (`min`/`max`), `boolean` (`"0"`/`"1"`), `url` (https,
|
|
69
|
+
http on loopback only), `originList`, `enum` (`values`) and `json` (object or array); it reuses
|
|
70
|
+
the `src/envKnobs.ts` readers. `loadVariables` returns a frozen object whose type is inferred
|
|
71
|
+
from the declaration (an undeclared key does not compile, and reading one at runtime throws,
|
|
72
|
+
naming it), or throws ONE `ConfigurationError` whose `problems` list every missing or invalid
|
|
73
|
+
variable (`{ key, env, reason, expected }`) and whose FR+EN message names them, never a value.
|
|
74
|
+
A broken declaration (duplicate env name, unknown type or property, a default that does not
|
|
75
|
+
validate, a default on a required variable) is a `VariableDeclarationError` at definition.
|
|
76
|
+
`describeVariables` lists the contract without values, for `--help` or docs. Serialising the
|
|
77
|
+
result (`JSON.stringify`, `String()`, template literals, `util.inspect`) never throws and shows a
|
|
78
|
+
`secret: true` value as the fixed mask `[secret]`; a secret is non-enumerable, so spread,
|
|
79
|
+
`Object.entries` and `Object.keys` never copy it, and it is read by name only. The boilerplate's
|
|
80
|
+
own readers (`OAUTH_ENV`, the envKnobs knobs, `clientCredentialsFromEnv`) are left as they are:
|
|
81
|
+
candidates to migrate onto the registry, not migrated here.
|
|
82
|
+
- **Outbound OAuth 2 `client_credentials` token source** (`src/upstream/clientCredentials.ts`,
|
|
83
|
+
exported from the package root) for a derived server that reads a third-party upstream API.
|
|
84
|
+
`createClientCredentialsTokenSource` caches the token until `expires_in` minus a skew (default
|
|
85
|
+
60 s; 300 s assumed when `expires_in` is absent), shares one in-flight token request between
|
|
86
|
+
concurrent callers, and `fetchWithToken` attaches the bearer token with one refetch and one
|
|
87
|
+
retry on a 401. `client_secret_basic` (default) or `client_secret_post`. A token endpoint
|
|
88
|
+
failure or an unreadable response is an `UpstreamTokenError` naming the status and the OAuth
|
|
89
|
+
`error` code, never a credential or a token. `clientCredentialsFromEnv` reads the variable
|
|
90
|
+
names the caller chooses and names any missing one, never its value. Create it once at module
|
|
91
|
+
scope (the server is rebuilt per request); both READMEs show the pattern.
|
|
92
|
+
The token request refuses redirects (`redirect: "error"`), so a 307/308 never re-sends the secret.
|
|
93
|
+
- **`/register` budget per IPv6 /48** (`MCP_REGISTER_LIMIT_PER_48_PER_MINUTE`, default 100). Every
|
|
94
|
+
/64 inside one /48 shares it, beside the per-/64 budget, so one /48 cannot fill the client
|
|
95
|
+
registry to its 503. An invalid value stops the server at start.
|
|
96
|
+
- A test pins that a denial with a forged `consentToken` is refused with no redirect; the deny
|
|
97
|
+
comment now states that the package verifies the token (cloud-identity >= 0.18).
|
|
98
|
+
- **Server manifest** `mcp-server.json` (mcp-standard C-18): capabilities, transport, version and
|
|
99
|
+
feature switches in one file. `src/serverManifest.ts` loads it fail-closed, refusing an unreadable
|
|
100
|
+
field by name (FR+EN in one message). The factory serves its capabilities;
|
|
101
|
+
`scripts/sync-plugin-version.mjs` reads the version from it and refuses a `package.json` that
|
|
102
|
+
disagrees, naming both. Bump `package.json` and `mcp-server.json` together.
|
|
103
|
+
- **Typed tool record** (`src/toolRecord.ts`, C-8, C-9, C-10, C-12, C-13): every tool declares in
|
|
104
|
+
its `annotations` the `authRead`/`authWrite` pair, the `verb`, the `table`, the `searchMechanism`
|
|
105
|
+
(`indexed-range`, never `unindexed-scan`) and `tokensEstimeContexte`, plus a `CONTEXT_CEILING`
|
|
106
|
+
(global and per tool). The sets are closed TypeScript unions. Served on the wire in both protocol
|
|
107
|
+
eras (the SDK `Client` drops unknown annotation keys; the tests read the raw result).
|
|
108
|
+
- **Cursor pagination** on `list_items` (C-11, S-12): `limit` (default 20,
|
|
109
|
+
capped at 100) and an opaque `cursor` over the key-sorted collection; `nextCursor` and
|
|
110
|
+
`exhausted` tell a truncated page from the last one. An invalid or expired cursor is a TOOL INPUT
|
|
111
|
+
error, not a protocol error: a result with `isError: true` and the stable machine code
|
|
112
|
+
`invalid_cursor` in `structuredContent.error.code`, with an FR+EN message via `X-MCP-Locale`;
|
|
113
|
+
never a silent first page. **C-11 scope split:** the spec 2025-11-25 changelog (SEP-1303) says
|
|
114
|
+
input validation errors are returned as Tool Execution Errors, so JSON-RPC `-32602` stays only
|
|
115
|
+
with the cursor of the protocol list methods, which the SDK serves (S-12). The standard amendment
|
|
116
|
+
is tracked in task `k178z7mdt51jchj82e10jq1qp18g36s4`.
|
|
117
|
+
- **Build identity** (C-25, S-7): `buildId`, a hash of the files the process runs from, beside
|
|
118
|
+
`version` on the unauthenticated `/health` and in the server identity of `server/discover` and
|
|
119
|
+
`initialize`. It changes when `dist/` changes.
|
|
120
|
+
- **Release workflow** `.github/workflows/release.yml` (C-17): `npm publish --provenance`, then the
|
|
121
|
+
`dist-tag` re-read at the registry with the cache bypassed, then the GitHub release. GitHub
|
|
122
|
+
Actions is disabled across the fleet; the README documents the equivalent without CI (packed
|
|
123
|
+
tarball checksum against `dist.shasum`).
|
|
124
|
+
- README (EN+FR): exact-version pin, rollback line and verification (C-7).
|
|
125
|
+
- Evals: `list_items` has eight cases, covering the page limit, a truncated page, the
|
|
126
|
+
invalid-cursor failure path, the `ui://` link and the French fallback. The eval harness reads the
|
|
127
|
+
JSON body from the first text block that parses and gains a `resourceUri` assertion.
|
|
128
|
+
|
|
129
|
+
### Removed (BREAKING for forks)
|
|
130
|
+
|
|
131
|
+
- **The `render_items_ui` tool is removed; `list_items` carries the UI.** One operation on one
|
|
132
|
+
table is one tool (mcp-standard C-12): `render_items_ui` and `list_items` shared the triplet
|
|
133
|
+
(`list`, `items`, `indexed-range`). `list_items` now holds `_meta.ui.resourceUri` (plus the
|
|
134
|
+
additive `openai/outputTemplate` keys), takes an optional `locale`, and returns
|
|
135
|
+
`structuredContent` (`items`, `locale`, `exhausted`, `nextCursor`) with a markdown fallback first
|
|
136
|
+
(A-1) and the `ui://` resource. The `ui://` resource is kept and renders the `list_items` result.
|
|
137
|
+
A fork that calls or documents `render_items_ui` calls `list_items` instead. The seed is still 0.x.
|
|
138
|
+
|
|
139
|
+
### Changed
|
|
140
|
+
|
|
141
|
+
- **`@vantageos/cloud-identity` `^0.19.1` -> `^0.20.2`** for `resolveCallerFromClaims` (tagged
|
|
142
|
+
`@callerResolver`), the package entry from verified claims to the calling person. No breaking
|
|
143
|
+
change for this boilerplate: the runtime modules that changed between the two versions
|
|
144
|
+
(`org-by-id`, `person-principal`, `principal-by-id`, plus new exports on the index) export none of
|
|
145
|
+
`requireTenantId`, `isRowInTenant`, `unauthorizedChallenge`, `verifyMcpAccessToken`,
|
|
146
|
+
`buildProtectedResourceMetadata`, the authorize flow or the token exchange. The package's own
|
|
147
|
+
dependencies are unchanged, so only its lockfile entry moves.
|
|
148
|
+
|
|
149
|
+
- `list_items` now returns one page (`exhausted`, optional `nextCursor`)
|
|
150
|
+
instead of every row, and its call result carries a markdown fallback, the JSON mirror and the
|
|
151
|
+
`ui://` resource; a call with no argument still returns the whole seed collection (three
|
|
152
|
+
rows, under the default page size).
|
|
153
|
+
- `TOOL_ANNOTATIONS` (four booleans shared by all tools) is replaced by one record per tool in
|
|
154
|
+
`src/toolRecord.ts`.
|
|
155
|
+
- `/health` answers `{ status, version, buildId }` instead of `{ status }`.
|
|
156
|
+
|
|
157
|
+
## [0.2.4] - 2026-10-11
|
|
158
|
+
|
|
159
|
+
### Changed
|
|
160
|
+
|
|
161
|
+
- The consent / organisation picker page uses the VantagePeers dashboard design tokens: dark by
|
|
162
|
+
default, light under `prefers-color-scheme`, one option card per organisation with its role
|
|
163
|
+
(`admin`, not `(org:admin)`), a primary Approve button. It still loads no external asset; the
|
|
164
|
+
single inline style is hashed into the page CSP.
|
|
165
|
+
|
|
166
|
+
### Added
|
|
167
|
+
|
|
168
|
+
- `AUTHORIZE_BRAND_NAME` (optional): a display name shown above the consent page title. None by
|
|
169
|
+
default, so a fork carries no brand it did not set.
|
|
170
|
+
|
|
10
171
|
## [0.2.3] - 2026-10-11
|
|
11
172
|
|
|
12
173
|
### Added
|
package/README.fr.md
CHANGED
|
@@ -10,8 +10,8 @@ serveur d'autorisation OAuth 2.1** (une personne Clerk choisit une organisation)
|
|
|
10
10
|
une UI MCP Apps et des messages FR/EN. Aucune route REST personnalisée ne se place
|
|
11
11
|
jamais devant un appel d'outil.
|
|
12
12
|
|
|
13
|
-
Il embarque
|
|
14
|
-
|
|
13
|
+
Il embarque deux petits outils d'exemple (`ping`, `list_items`) qui exercent
|
|
14
|
+
chaque couche ; `list_items` porte aussi le widget MCP Apps. Remplacez-les par les vôtres ; le câblage reste.
|
|
15
15
|
|
|
16
16
|
## Structure
|
|
17
17
|
|
|
@@ -19,16 +19,24 @@ qui exercent chaque couche. Remplacez-les par les vôtres ; le câblage reste.
|
|
|
19
19
|
src/
|
|
20
20
|
mcpServerFactory.ts # définition unique du serveur MCP, partagée par les deux transports
|
|
21
21
|
version.ts # nom + version du serveur, dérivés de package.json
|
|
22
|
+
serverManifest.ts # chargeur fail-closed de mcp-server.json (le manifeste serveur)
|
|
23
|
+
toolRecord.ts # fiche d'outil typée : autorité, verbe, table, mécanisme, pagination, coût
|
|
24
|
+
pagination.ts # pagination par curseur appliquée par l'outil (défaut, max fini, invalid_cursor)
|
|
25
|
+
buildIdentity.ts # identité de build dérivée de l'arbre servi
|
|
22
26
|
stdio.ts # serveStdio(factory) : les deux ères du protocole sur stdio
|
|
23
27
|
http.ts # adaptateur Node http.Server autour de l'app Hono
|
|
24
28
|
httpApp.ts # createMcpHandler(factory) sur /mcp + la barrière OAuth
|
|
25
29
|
auth.ts # câblage resource-server (@vantageos/cloud-identity)
|
|
26
30
|
config.ts # AuthMode + AuthConfig d'émetteur externe, dérivés de l'environnement
|
|
27
31
|
tenancy.ts # JWT vérifié -> tenant ; lignes filtrées par orgId stocké
|
|
32
|
+
lake/ # lecteur de data-lake en lecture seule : interface + défaut en mémoire, adaptateur Convex
|
|
33
|
+
reader.ts convex.ts
|
|
28
34
|
oauth/ # serveur d'autorisation propre (cloud-identity décide)
|
|
29
35
|
authorizationServer.ts settings.ts clerk.ts tokenIssuer.ts
|
|
30
36
|
clientRegistry.ts codeStore.ts picker.ts
|
|
31
37
|
ui/ # widget MCP Apps + ressource ui://
|
|
38
|
+
mcp-server.json # le manifeste serveur : capabilities, transport, version, interrupteurs de fonctionnalités
|
|
39
|
+
.github/workflows/release.yml # npm publish --provenance + preuve au registre (Actions est désactivé dans la flotte)
|
|
32
40
|
skills/connect-mcp-server/SKILL.md # skill d'onboarding (anglais)
|
|
33
41
|
scripts/
|
|
34
42
|
plugin-manifest.source.json # l'UNIQUE source de tous les manifestes de packaging
|
|
@@ -74,13 +82,34 @@ lue dans `package.json`.
|
|
|
74
82
|
|
|
75
83
|
## Fiches d'outils
|
|
76
84
|
|
|
77
|
-
Chaque outil déclare `title`,
|
|
78
|
-
(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
Chaque outil déclare `title`, un schéma d'entrée et un schéma de sortie typés
|
|
86
|
+
(Zod 4), et renvoie `structuredContent` plus un bloc texte portant le même JSON. Ses
|
|
87
|
+
`annotations` portent la fiche typée de `src/toolRecord.ts`, lisible depuis
|
|
88
|
+
`tools/list` sans lire la source :
|
|
89
|
+
|
|
90
|
+
| Clé | Règle | Sens |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint` | C-15 | les quatre booléens de comportement |
|
|
93
|
+
| `authRead`, `authWrite` | C-8 | couple d'autorité parmi `public` / `org-member` / `org-admin` / `fleet-internal` / `master`, distincts ; résolu depuis l'identité vérifiée, jamais depuis un argument |
|
|
94
|
+
| `verb`, `table` | C-9, C-12 | l'opération (`list`, `get`, `create`, `pure`...) et la donnée touchée |
|
|
95
|
+
| `searchMechanism` | C-10 | comment une liste/recherche est servie (`indexed-range` ici) ; `unindexed-scan` ne se publie pas |
|
|
96
|
+
| `paginationCursor`, `paginationDefault`, `paginationMax` | C-11 | curseur, taille de page par défaut et maximum fini, appliqués par `src/pagination.ts` |
|
|
97
|
+
| `tokensEstimeContexte` | C-13 | coût de contexte estimé ; `CONTEXT_CEILING` borne chaque outil et la somme |
|
|
98
|
+
|
|
99
|
+
Les ensembles sont des unions TypeScript fermées : une valeur hors ensemble ne
|
|
100
|
+
compile pas, et un outil de liste ne peut omettre ni son mécanisme ni sa pagination.
|
|
101
|
+
La fiche de chaque outil est un objet de littéraux, lisible par un contrôleur
|
|
102
|
+
statique. `list_items` pagine ses lignes : `limit` (20 par
|
|
103
|
+
défaut, plafonné à 100) et un `cursor` opaque ; une page tronquée porte `nextCursor`
|
|
104
|
+
et `exhausted: false`, la dernière page `exhausted: true` ; un curseur invalide ou expiré est
|
|
105
|
+
une erreur d'entrée de l'outil : un résultat `isError: true` avec
|
|
106
|
+
`structuredContent.error.code` valant `invalid_cursor` (message FR/EN), jamais une
|
|
107
|
+
première page silencieuse. Le `-32602` JSON-RPC reste réservé au curseur des méthodes
|
|
108
|
+
de liste du protocole, servies par le SDK.
|
|
109
|
+
|
|
110
|
+
`tests/toolRecordComplete.test.ts` et les suites C-8..C-13 parcourent `tools/list` et
|
|
111
|
+
échouent au moindre manque. Les annotations sont des indications non fiables pour le
|
|
112
|
+
client (mcp-standard C-15) : ne conditionnez jamais une action sur elles seules.
|
|
84
113
|
|
|
85
114
|
## Paliers d'authentification
|
|
86
115
|
|
|
@@ -130,7 +159,15 @@ déploiement mono-instance. Derrière un reverse proxy, réglez `MCP_TRUSTED_PRO
|
|
|
130
159
|
sur le nombre de proxys de confiance (défaut `0`) : la limite de débit de `/register`
|
|
131
160
|
se fonde alors sur l'entrée `X-Forwarded-For` ajoutée par le proxy, jamais sur la
|
|
132
161
|
première, fournie par le client ; avec `0`, elle se fonde sur l'adresse du socket et
|
|
133
|
-
ignore l'en-tête.
|
|
162
|
+
ignore l'en-tête. La valeur est le nombre de SAUTS de proxy qui ajoutent une entrée à
|
|
163
|
+
`X-Forwarded-For`, pas le nombre de produits déployés : Railway demande `2` (mesuré : son
|
|
164
|
+
en-tête porte le client, puis le proxy d'entrée). Une valeur trop BASSE échoue en silence,
|
|
165
|
+
car le limiteur se fonde alors sur l'adresse d'un proxy partagée par de nombreux clients.
|
|
166
|
+
Pour la mesurer, envoyez 21 enregistrements depuis un même client et attendez un `429` au
|
|
167
|
+
21e. Un client IPv6 est compté sur son /64 ET sur son /48 : tous les /64 d'un même /48 se
|
|
168
|
+
partagent `MCP_REGISTER_LIMIT_PER_48_PER_MINUTE` enregistrements par minute (défaut `100`),
|
|
169
|
+
si bien qu'un /48, qui contient 65536 /64, ne peut pas remplir le registre de clients jusqu'à
|
|
170
|
+
son `503`. Une valeur invalide arrête le serveur au démarrage.
|
|
134
171
|
|
|
135
172
|
**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`
|
|
136
173
|
(défaut 1 Mio) du corps d'une requête et répond `413` avant tout handler.
|
|
@@ -194,6 +231,11 @@ fonctionne sur un serveur que vous hébergez, dans cet ordre :
|
|
|
194
231
|
nomme l'exigence de Home URL. C'est un avertissement, jamais un refus : il signifie « une
|
|
195
232
|
connexion distante ne marchera que si la Home URL pointe sur ce serveur, ou si vous passez à
|
|
196
233
|
une instance de production ». Il ne contient ni clé ni secret.
|
|
234
|
+
6. **La page de consentement.** Le choix d'organisation reprend le design du tableau de bord
|
|
235
|
+
VantagePeers : sombre par défaut, clair quand le système de la personne le demande, une
|
|
236
|
+
carte par organisation avec son rôle. Elle ne charge aucune ressource externe et porte donc
|
|
237
|
+
ses propres tokens. Réglez `AUTHORIZE_BRAND_NAME` pour afficher le nom de votre produit
|
|
238
|
+
au-dessus du titre ; rien n'est affiché par défaut.
|
|
197
239
|
|
|
198
240
|
### Multi-tenance
|
|
199
241
|
|
|
@@ -202,6 +244,220 @@ vérifiée de l'appelant (`isRowInTenant`) ; un jeton sans organisation reçoit
|
|
|
202
244
|
refus d'outil explicite, jamais une vue non filtrée. Ne résolvez le tenant de
|
|
203
245
|
l'appelant qu'à partir du jeton vérifié, jamais d'un argument d'outil.
|
|
204
246
|
|
|
247
|
+
### La personne vérifiée dans un outil
|
|
248
|
+
|
|
249
|
+
Un outil peut aussi avoir besoin de la PERSONNE derrière l'appel (sujet, rôle, statut
|
|
250
|
+
d'administrateur), par exemple pour filtrer des lignes par personne. La personne est résolue une
|
|
251
|
+
fois par requête `/mcp` par `resolveCallerFromClaims` de `@vantageos/cloud-identity`, à partir des
|
|
252
|
+
mêmes claims vérifiés que le tenant ; le serveur ne lit jamais un claim lui-même. Le paquet lit
|
|
253
|
+
deux magasins que vous injectez, et les rôles d'administrateur sont à vous de les nommer
|
|
254
|
+
(obligatoires, non vides, aucun défaut) :
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
import { buildHttpApp } from "@vantageos/mcp-boilerplate";
|
|
258
|
+
|
|
259
|
+
const app = buildHttpApp({
|
|
260
|
+
authMode: "oauth",
|
|
261
|
+
authorizationServer,
|
|
262
|
+
identity: {
|
|
263
|
+
orgLookups: { orgById: (orgId) => orgStore.get(orgId) },
|
|
264
|
+
principalLookups: {
|
|
265
|
+
personById: (personId, orgId) => people.get(orgId, personId),
|
|
266
|
+
organisationById: (orgId) => orgStore.row(orgId),
|
|
267
|
+
orgKindOf: (orgId) => orgStore.kind(orgId),
|
|
268
|
+
},
|
|
269
|
+
adminRoles: ["org:admin"],
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Un outil déclare qu'il exige la personne en enveloppant son handler avec `withVerifiedPerson`
|
|
275
|
+
(dans `src/mcpServerFactory.ts`, où `deps` est le contexte par requête dans lequel voyage le
|
|
276
|
+
tenant) :
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
server.registerTool("my_rows", config, withVerifiedPerson(deps, async (args, person) => {
|
|
280
|
+
// person : { subject, actorName, org, principal, role, isOrgAdmin }, gelée, telle que le paquet l'a rendue
|
|
281
|
+
return structuredResult({ rows: rowsVisibleTo(person.subject) });
|
|
282
|
+
}));
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Sauf si la personne est résolue, le handler ne s'exécute pas : l'appel reçoit une erreur d'outil
|
|
286
|
+
MCP au code stable `person_unresolved` (`structuredContent.error.code`), un message FR/EN selon
|
|
287
|
+
`X-MCP-Locale`, et une `reason` : `identity_not_configured` (aucune `identity` injectée),
|
|
288
|
+
`no_verified_session` (palier public, et **stdio, qui n'a jamais de personne** : ce transport ne
|
|
289
|
+
porte aucun bearer) ou `refused` (le paquet a refusé ; son `code` et sa `reason` sont transmis,
|
|
290
|
+
jamais une valeur de claim). Un outil qui n'enveloppe pas son handler s'exécute exactement comme
|
|
291
|
+
avant, sans personne.
|
|
292
|
+
|
|
293
|
+
## Appeler une API amont (OAuth 2 client_credentials)
|
|
294
|
+
|
|
295
|
+
Un serveur dérivé de ce boilerplate qui lit une API tierce avec ses propres identifiants utilise
|
|
296
|
+
`createClientCredentialsTokenSource` (`src/upstream/clientCredentials.ts`). Créez-le UNE fois,
|
|
297
|
+
au niveau du module : le serveur MCP est reconstruit à chaque requête, et le cache du jeton vit
|
|
298
|
+
dans la source.
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
const upstream = createClientCredentialsTokenSource(
|
|
302
|
+
clientCredentialsFromEnv({
|
|
303
|
+
clientIdVar: "UPSTREAM_CLIENT_ID",
|
|
304
|
+
clientSecretVar: "UPSTREAM_CLIENT_SECRET",
|
|
305
|
+
tokenUrlVar: "UPSTREAM_TOKEN_URL",
|
|
306
|
+
})
|
|
307
|
+
);
|
|
308
|
+
// dans un handler d'outil :
|
|
309
|
+
const res = await upstream.fetchWithToken("https://api.upstream.example/v1/things");
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Les noms de variables sont à votre choix. Le jeton est mis en cache jusqu'à `expires_in` moins
|
|
313
|
+
une marge de 60 s, les appels simultanés partagent une seule requête de jeton, et un 401
|
|
314
|
+
déclenche un seul nouveau jeton et une seule nouvelle tentative, jamais plus. L'authentification
|
|
315
|
+
du client est `client_secret_basic` par défaut, ou `client_secret_post`. Une variable manquante
|
|
316
|
+
est nommée au démarrage, sans jamais afficher sa valeur ; un échec du point de jeton est une
|
|
317
|
+
`UpstreamTokenError` portant le statut et le code OAuth `error`, jamais un identifiant ni un
|
|
318
|
+
jeton.
|
|
319
|
+
|
|
320
|
+
### Lire toutes les pages d'un endpoint paginé par décalage
|
|
321
|
+
|
|
322
|
+
`createPaginatedUpstreamClient` (`src/upstream/paginatedClient.ts`) lit un endpoint amont paginé
|
|
323
|
+
par décalage (offset) et taille (limit). Les noms des paramètres, leur emplacement (`query` ou
|
|
324
|
+
`jsonBody`) et la méthode sont de la configuration. Créez-le une fois au niveau du module, à côté
|
|
325
|
+
de la source de jeton :
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
const things = createPaginatedUpstreamClient({
|
|
329
|
+
fetchImpl: upstream.fetchWithToken,
|
|
330
|
+
baseUrl: "https://api.upstream.example/v1/",
|
|
331
|
+
allowedOrigins: ["https://api.upstream.example"],
|
|
332
|
+
offsetParam: "offset",
|
|
333
|
+
limitParam: "limit",
|
|
334
|
+
placement: "jsonBody", // POST avec offset et limit dans le corps JSON ; "query" envoie un GET
|
|
335
|
+
rowCeiling: 5000,
|
|
336
|
+
extractRows: (body) => (body as { data: unknown[] }).data,
|
|
337
|
+
});
|
|
338
|
+
const all = await things.readAll("things", { status: "active" }); // toutes les pages, ou un refus
|
|
339
|
+
const page = await things.readPage("things", { status: "active" }, cursor); // une page par appel
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
`allowedOrigins` et `rowCeiling` sont obligatoires. Un chemin qui se résout vers une autre origine
|
|
343
|
+
est refusé avant tout appel réseau : le jeton ne part jamais vers un hôte nommé par un argument
|
|
344
|
+
d'outil, et les redirections sont refusées, jamais suivies (`redirect: "error"`). `readAll` s'arrête
|
|
345
|
+
sur la page incomplète (ou au total, avec `extractTotal`) et refuse un ensemble au-delà du plafond
|
|
346
|
+
avec `row_ceiling_exceeded`, jamais un ensemble tronqué en silence. `readPage` renvoie
|
|
347
|
+
`{ rows, nextCursor?, exhausted }` ; son curseur est lié au chemin et aux paramètres, et un curseur rejoué
|
|
348
|
+
sur une autre requête ou altéré sans recalcul de son empreinte est refusé en `invalid_cursor`.
|
|
349
|
+
L'empreinte n'est pas signée : elle détecte un mauvais usage, pas une falsification ; l'offset ne
|
|
350
|
+
confère aucun droit et un offset forgé au-delà du plafond est refusé avant toute requête. Une page
|
|
351
|
+
vide avant le total déclaré est refusée en `inconsistent_page`, jamais redemandée. Les 429 et 503
|
|
352
|
+
sont rejoués avec un délai exponentiel (5 tentatives par défaut, base 500 ms, plafond 30 s) et
|
|
353
|
+
respectent `Retry-After` ; aucun autre statut n'est rejoué. Les erreurs (`UpstreamPageError`)
|
|
354
|
+
nomment le chemin et le statut, jamais un jeton, une valeur de paramètre ni le corps. Gardez
|
|
355
|
+
`pageSize` (défaut 100) dans le maximum réel de l'amont : une page plafonnée en silence en dessous
|
|
356
|
+
se lit comme la dernière, sauf si la réponse déclare un total.
|
|
357
|
+
|
|
358
|
+
## Déclarer sa propre configuration
|
|
359
|
+
|
|
360
|
+
Un serveur dérivé déclare une fois les variables d'environnement qu'il lit, et reçoit au
|
|
361
|
+
démarrage un objet typé, validé et gelé (`src/declaredVariables.ts`) :
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
import { defineVariables, describeVariables, loadVariables } from "@vantageos/mcp-boilerplate";
|
|
365
|
+
|
|
366
|
+
export const declared = defineVariables({
|
|
367
|
+
apiBaseUrl: {
|
|
368
|
+
env: "UPSTREAM_API_BASE_URL",
|
|
369
|
+
type: "url",
|
|
370
|
+
description: { en: "Upstream API base URL", fr: "URL de base de l'API amont" },
|
|
371
|
+
},
|
|
372
|
+
readKey: {
|
|
373
|
+
env: "LAKE_READ_KEY",
|
|
374
|
+
type: "string",
|
|
375
|
+
secret: true,
|
|
376
|
+
description: { en: "Data lake read key", fr: "Clé de lecture du lac de données" },
|
|
377
|
+
},
|
|
378
|
+
pageSize: {
|
|
379
|
+
env: "UPSTREAM_PAGE_SIZE",
|
|
380
|
+
type: "integer",
|
|
381
|
+
min: 1,
|
|
382
|
+
max: 500,
|
|
383
|
+
required: false,
|
|
384
|
+
default: 100,
|
|
385
|
+
description: { en: "Rows per upstream page", fr: "Lignes par page amont" },
|
|
386
|
+
},
|
|
387
|
+
fieldMap: {
|
|
388
|
+
env: "FIELD_MAPPINGS",
|
|
389
|
+
type: "json",
|
|
390
|
+
description: { en: "Code-to-label mappings", fr: "Correspondances code-libellé" },
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
export const settings = loadVariables(declared); // settings.pageSize : number
|
|
395
|
+
console.log(describeVariables(declared)); // le contrat, pour --help : aucune valeur, aucun défaut
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Les noms de variables sont à votre choix. Types : `string`, `integer` (`min`/`max`), `boolean`
|
|
399
|
+
(`"0"` ou `"1"`), `url` (https, http seulement sur la boucle locale), `originList`, `enum`
|
|
400
|
+
(`values`) et `json` (un objet ou un tableau). Une variable est requise sauf si elle indique
|
|
401
|
+
`required: false` ; un `default` exige `required: false`. Absente ou vide, elle est manquante ;
|
|
402
|
+
une valeur définie mais invalide est refusée, jamais remplacée par le défaut. Toutes les variables
|
|
403
|
+
manquantes ou invalides sont signalées ensemble dans UNE `ConfigurationError`
|
|
404
|
+
(`problems: { key, env, reason, expected }[]`, message en anglais et en français) qui les nomme
|
|
405
|
+
sans jamais afficher de valeur. Le résultat est gelé et typé d'après la déclaration : une clé non
|
|
406
|
+
déclarée ne compile pas, et sa lecture à l'exécution lève une erreur. `JSON.stringify`, `String()`
|
|
407
|
+
et `console.log` du résultat ne lèvent jamais d'erreur et affichent une valeur `secret: true`
|
|
408
|
+
sous la forme `[secret]` ; un secret est ignoré par la décomposition (`...`) et `Object.entries`,
|
|
409
|
+
et se lit uniquement par son nom. Une déclaration cassée (nom de variable en double, type ou
|
|
410
|
+
propriété inconnus, défaut qui ne se valide pas) est refusée dès l'appel à `defineVariables`. Les
|
|
411
|
+
variables propres au boilerplate (`OAUTH_ENV`, les réglages du serveur, `clientCredentialsFromEnv`)
|
|
412
|
+
n'utilisent pas encore ce registre.
|
|
413
|
+
|
|
414
|
+
## Lire le data-lake du client
|
|
415
|
+
|
|
416
|
+
Un serveur dérivé lit le data-lake de son client (l'unique source persistée) à travers une
|
|
417
|
+
interface en LECTURE SEULE, `LakeReader` (`src/lake/reader.ts`) : `listPage({ after?, limit, scope? })`
|
|
418
|
+
renvoie `{ rows, exhausted }` dans l'ordre croissant des clés, `get(key, scope?)` renvoie une ligne
|
|
419
|
+
ou `undefined`, et `keyOf(row)` désigne la clé stable que porte le curseur. Il n'existe aucune
|
|
420
|
+
méthode d'écriture. `list_items` lit à travers le lecteur injecté par `buildHttpApp({ lake })` (ou
|
|
421
|
+
`McpServerDeps.lake`) ; sans injection, il lit les données d'exemple en mémoire
|
|
422
|
+
(`EXAMPLE_ITEMS_READER`), exactement comme avant. Le tenant de l'appelant voyage DANS la lecture
|
|
423
|
+
sous la forme de `scope`, et chaque ligne renvoyée est revérifiée : une ligne d'un autre tenant fait
|
|
424
|
+
échouer l'appel, fermé. `readPage` applique les bornes et le curseur de l'outil à n'importe quel
|
|
425
|
+
lecteur et contrôle chaque page contre le contrat ; une page illisible (pas une liste, plus de
|
|
426
|
+
lignes que demandé, clés désordonnées ou non postérieures au curseur, page tronquée sans ligne,
|
|
427
|
+
ligne sans clé) est une `LakeReadError`, à laquelle `list_items` répond par une erreur d'outil au
|
|
428
|
+
code stable `lake_unreadable` avec la raison, jamais par une page vide ou partielle.
|
|
429
|
+
|
|
430
|
+
L'adaptateur Convex est livré sous son propre sous-chemin ; `convex` est une dépendance pair
|
|
431
|
+
optionnelle, installée seulement par un serveur qui l'importe :
|
|
432
|
+
|
|
433
|
+
```ts
|
|
434
|
+
import { convexLakeReaderFromEnv } from "@vantageos/mcp-boilerplate/lake/convex";
|
|
435
|
+
|
|
436
|
+
const lake = convexLakeReaderFromEnv(
|
|
437
|
+
process.env,
|
|
438
|
+
{ urlVar: "LAKE_URL", tokenVar: "LAKE_READ_KEY" },
|
|
439
|
+
{
|
|
440
|
+
functions: { page: "lakeRead:listRows", get: "lakeRead:getRow" },
|
|
441
|
+
keyField: "rowKey",
|
|
442
|
+
tokenArg: "readKey", // à omettre pour envoyer le jeton par setAuth (Authorization: Bearer)
|
|
443
|
+
}
|
|
444
|
+
);
|
|
445
|
+
const app = buildHttpApp({ authMode: "oauth", authorizationServer, lake });
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
Les noms de variables, les noms des fonctions de requête et le champ clé sont de la configuration :
|
|
449
|
+
rien dans le paquet ne nomme une table ou une fonction. Les variables de connexion sont déclarées
|
|
450
|
+
avec `defineVariables` (l'URL en `url` : https, http seulement sur la boucle locale ; le jeton en
|
|
451
|
+
`secret`), si bien qu'une variable manquante ou invalide est refusée au démarrage, par son nom,
|
|
452
|
+
jamais par sa valeur. L'adaptateur n'appelle que `ConvexHttpClient.query`, jamais une mutation ni
|
|
453
|
+
une action. La fonction de page reçoit `{ after?, limit, tenantId? }` et renvoie les lignes dont la
|
|
454
|
+
clé suit `after`, par ordre croissant, au plus `limit` (l'adaptateur demande une ligne de plus que
|
|
455
|
+
la page pour distinguer une page tronquée de la dernière) ; la fonction de lecture unitaire reçoit
|
|
456
|
+
`{ key, tenantId? }` et renvoie la ligne ou `null`. Un `parseRow` optionnel (par exemple un `parse`
|
|
457
|
+
Zod) type chaque ligne et refuse celle qu'il ne sait pas lire. Un appel en échec est `read_failed`,
|
|
458
|
+
et le message du client n'est pas relayé : aucune erreur ne porte le jeton, l'URL ni une valeur de
|
|
459
|
+
ligne.
|
|
460
|
+
|
|
205
461
|
## i18n côté serveur (FR/EN)
|
|
206
462
|
|
|
207
463
|
`src/i18n.ts` localise les messages d'erreur/validation MCP et la page de sélection :
|
|
@@ -223,8 +479,10 @@ stdio.
|
|
|
223
479
|
|
|
224
480
|
## Ajouter un outil
|
|
225
481
|
|
|
226
|
-
|
|
227
|
-
|
|
482
|
+
Déclarez sa fiche dans `src/toolRecord.ts` (couple d'autorité, verbe, table, et pour
|
|
483
|
+
une liste/recherche le mécanisme et la pagination), enregistrez-le une fois dans
|
|
484
|
+
`src/mcpServerFactory.ts` avec `title`, `annotations: VOS_ANNOTATIONS`,
|
|
485
|
+
`inputSchema` et `outputSchema`, et renvoyez `structuredResult(...)`. Il
|
|
228
486
|
est alors servi sur les deux transports et les deux ères du protocole.
|
|
229
487
|
`tests/toolRecordComplete.test.ts` le garde honnête.
|
|
230
488
|
|
|
@@ -262,10 +520,60 @@ npm publish # prepublishOnly relance build + test + evals
|
|
|
262
520
|
La publication se prouve par le `dist-tag` du registre, jamais par le code de sortie
|
|
263
521
|
de `npm publish`.
|
|
264
522
|
|
|
523
|
+
La version vit dans `package.json` ET dans `mcp-server.json` (le manifeste serveur,
|
|
524
|
+
mcp-standard C-18). Montez les deux à la même valeur : `scripts/sync-plugin-version.mjs`
|
|
525
|
+
lit la version dans le manifeste et refuse un `package.json` qui diffère, en nommant
|
|
526
|
+
les deux. Le manifeste déclare aussi les capabilities du serveur, que la fabrique
|
|
527
|
+
sert, et les interrupteurs de fonctionnalités que les versions suivantes liront.
|
|
528
|
+
|
|
529
|
+
## Épingler et vérifier le paquet publié
|
|
530
|
+
|
|
531
|
+
Installez depuis npm, le registre de référence, et épinglez une version EXACTE pour
|
|
532
|
+
qu'une mise à jour n'arrive jamais sans l'avoir demandée. `1.2.3` ci-dessous est un
|
|
533
|
+
exemple, pas une version publiée : listez les vraies et remplacez-la par la version
|
|
534
|
+
que vous avez auditée.
|
|
535
|
+
|
|
536
|
+
```bash
|
|
537
|
+
npm view @vantageos/mcp-boilerplate versions --json # les versions publiées
|
|
538
|
+
npm install @vantageos/mcp-boilerplate@1.2.3 # épingler une version exacte
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
Le retour arrière consiste à épingler la version précédente ; aucune version publiée
|
|
542
|
+
n'est jamais retirée.
|
|
543
|
+
|
|
544
|
+
```bash
|
|
545
|
+
npm install @vantageos/mcp-boilerplate@1.2.2 # retour arrière : la version précédente
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
Un second canal indépendant porte le même tag : la GitHub release de
|
|
549
|
+
`elpiarthera/vantage-mcp-boilerplate`. Le paquet est publié avec
|
|
550
|
+
`npm publish --provenance` par `.github/workflows/release.yml` ; vérifiez
|
|
551
|
+
l'attestation avec `npm audit signatures`.
|
|
552
|
+
|
|
553
|
+
GitHub Actions est désactivé dans toute la flotte ElPi Corp : une publication faite
|
|
554
|
+
sans CI n'a pas d'attestation de provenance. La vérification équivalente est la somme
|
|
555
|
+
de contrôle du tarball empaqueté, reproduite depuis un checkout propre du tag puis
|
|
556
|
+
relue au registre :
|
|
557
|
+
|
|
558
|
+
```bash
|
|
559
|
+
npm pack # affiche le nom du tarball
|
|
560
|
+
shasum mcp-boilerplate-1.2.3.tgz # la somme que vous reproduisez (nom réel)
|
|
561
|
+
curl -s -H 'Cache-Control: no-cache' https://registry.npmjs.org/@vantageos%2Fmcp-boilerplate/1.2.3 \
|
|
562
|
+
| node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).dist.shasum"
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
Sommes égales : le registre sert ce que la source construit. Un serveur en marche
|
|
566
|
+
prouve de la même façon ce qu'il sert : `GET /health` renvoie
|
|
567
|
+
`{ status, version, buildId }` avant toute authentification. `buildId` est un hachage
|
|
568
|
+
des fichiers dont le processus s'exécute ; il change quand ils changent, ce que
|
|
569
|
+
`version` ne garantit pas.
|
|
570
|
+
|
|
265
571
|
## Couche UI MCP Apps
|
|
266
572
|
|
|
267
|
-
`
|
|
268
|
-
(`io.modelcontextprotocol/ui`, spec 2026-01-26).
|
|
573
|
+
`list_items` rend sa page de `EXAMPLE_ITEMS` sous forme de widget interactif
|
|
574
|
+
(`io.modelcontextprotocol/ui`, spec 2026-01-26). Une opération sur une table est un
|
|
575
|
+
seul outil (mcp-standard C-12) : l'outil de données porte lui-même le lien UI, il
|
|
576
|
+
n'y a pas d'outil de rendu séparé.
|
|
269
577
|
|
|
270
578
|
- L'outil relie sa ressource `ui://` par la clé imbriquée `_meta.ui.resourceUri`
|
|
271
579
|
avec `visibility: ["model", "app"]` explicite. L'outil est enregistré avec
|
|
@@ -289,7 +597,7 @@ en un seul fichier HTML autonome (un plugin d'inlining local dans `vite.config.t
|
|
|
289
597
|
`dist/ui-bundle/widget.html`) par `npm run build:ui`, sur un chemin qui n'entre
|
|
290
598
|
jamais en collision avec `dist/ui/` de tsc.
|
|
291
599
|
|
|
292
|
-
**Couche d'identité** : `@vantageos/cloud-identity` (^0.
|
|
600
|
+
**Couche d'identité** : `@vantageos/cloud-identity` (^0.20.2) est le seul paquet
|
|
293
601
|
d'identité. Son point d'entrée resource-server (`verifyMcpAccessToken`,
|
|
294
602
|
`buildProtectedResourceMetadata`) vérifie le jeton Bearer sur `/mcp` ; `jose` ne sert
|
|
295
603
|
plus qu'à signer les jetons d'accès du serveur d'autorisation. Un jeu de clés
|