@vantageos/mcp-boilerplate 0.1.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.
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "vantage-mcp-boilerplate",
3
+ "version": "0.1.0",
4
+ "description": "Clonable dual-transport MCP server template (stdio + Streamable HTTP, stateless) on the official @modelcontextprotocol/sdk, with protocol-kit result types, OAuth 2.1 auth + public tier, multi-tenant JWT scoping, MCP Apps UI extension, server-side FR/EN i18n, and a declarative eval harness. Scaffold, extend, publish.",
5
+ "description_fr": "Template de serveur MCP clonable a double transport (stdio + Streamable HTTP, stateless) sur le @modelcontextprotocol/sdk officiel, avec types de resultats protocol-kit, auth OAuth 2.1 + tier public, scoping multi-tenant par JWT, extension UI MCP Apps, i18n FR/EN cote serveur, et un harnais d'evaluations declaratif. Scaffolder, etendre, publier.",
6
+ "author": "ElPi Corp / Laurent Perello",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/elpiarthera/vantage-mcp-boilerplate",
9
+ "category": "mcp-server",
10
+ "bilingual": true,
11
+ "keywords": ["mcp", "claude-code", "boilerplate", "starter", "template", "stdio", "streamable-http", "oauth", "multi-tenant", "i18n", "vantage", "elpi"]
12
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ElPi Corp / Laurent Perello
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,183 @@
1
+ # @vantageos/mcp-boilerplate
2
+
3
+ A clonable MCP server skeleton: dual-transport bootstrap (stdio + Streamable
4
+ HTTP, stateless) built exclusively on the official
5
+ [`@modelcontextprotocol/sdk`](https://www.npmjs.com/package/@modelcontextprotocol/sdk)
6
+ transports. No custom REST routes ever stand in front of a tool call.
7
+
8
+ This is a foundation, not a finished product: it ships one trivial example
9
+ tool (`ping`) whose only purpose is to prove the MCP handshake works on both
10
+ transports. Later layers (protocol-kit result types, auth, multi-tenant
11
+ scoping, UI extensions, i18n) build on top of this bootstrap without
12
+ changing the transport wiring.
13
+
14
+ ## Structure
15
+
16
+ ```
17
+ src/
18
+ mcpServerFactory.ts # single MCP server definition, shared by both transports
19
+ stdio.ts # official SDK StdioServerTransport entrypoint
20
+ http.ts # Node http.Server adapter around the Hono app
21
+ httpApp.ts # official SDK WebStandardStreamableHTTPServerTransport (stateless)
22
+ auth.ts # OAuth 2.1 resource-server wiring (@vantageos/mcp-auth-kit)
23
+ config.ts # env-derived AuthMode + AuthConfig loaders
24
+ index.ts # package exports
25
+ tests/
26
+ helpers/mcpRpc.ts # JSON-RPC request helpers for the HTTP transport
27
+ helpers/authFixtures.ts # test-only RS256 JWT signing fixtures
28
+ httpHandshake.test.ts # initialize -> tools/list -> tools/call over Streamable HTTP
29
+ stdioHandshake.test.ts # same handshake over a real spawned stdio child process
30
+ oauth.test.ts # both auth tiers: oauth gate + public no-auth tier
31
+ docs/
32
+ architecture.md # wire posture: tool-level vs wire-level protocol version
33
+ ```
34
+
35
+ ## Quick start
36
+
37
+ ```bash
38
+ npm install
39
+ npm run build
40
+
41
+ # stdio (local client config, e.g. Claude Code / Cursor) — never gated,
42
+ # the local process invoking stdio IS the caller.
43
+ npm run start:stdio
44
+
45
+ # Streamable HTTP (remote, stateless) — OAuth 2.1 gate by default:
46
+ MCP_OAUTH_ISSUER=https://your-tenant.example.com \
47
+ MCP_OAUTH_AUDIENCE=https://your-mcp-server.example.com \
48
+ PORT=8787 npm run start:http
49
+
50
+ # Streamable HTTP, no-auth "Public MCP" tier (local/self-host, trusted caller):
51
+ AUTH_MODE=public PORT=8787 npm run start:http
52
+ ```
53
+
54
+ ## Authentication tiers
55
+
56
+ Two tiers, per `mcp-standard.md` "Authentication & Authorization":
57
+
58
+ | `AUTH_MODE` | Behavior |
59
+ |---|---|
60
+ | `oauth` (default) | OAuth 2.1 resource server (`@vantageos/mcp-auth-kit`): every `/mcp` HTTP call requires a valid RS256 Bearer JWT, audience/issuer-bound. Missing/invalid token -> `401` + `WWW-Authenticate`. RFC 9728 Protected Resource Metadata served at `/.well-known/oauth-protected-resource`. `/health` stays token-free. Requires `MCP_OAUTH_ISSUER` + `MCP_OAUTH_AUDIENCE`. |
61
+ | `public` | No gate at all on the HTTP transport — for local/self-host use where the caller is already trusted. Mirrors the stdio transport's trust model (stdio never had a gate). No `.well-known/oauth-protected-resource` document is served in this mode (there is no authorization server to describe). |
62
+
63
+ The stdio transport (`stdio.ts`) is never gated in either mode — the local
64
+ process invoking it IS the caller, per `mcp-standard.md`'s trust model for
65
+ local/self-host tools.
66
+
67
+ `buildHttpApp()` called with no arguments defaults to the `public` tier — this
68
+ is what the transport-skeleton tests (`httpHandshake.test.ts`,
69
+ `protocolKitWiring.test.ts`) exercise, so they need no OAuth configuration.
70
+ The production entrypoint (`http.ts`) always resolves `AUTH_MODE` from the
71
+ environment via `config.ts`, defaulting to `oauth` when unset.
72
+
73
+ ## Server-side i18n (FR/EN error messages)
74
+
75
+ `src/i18n.ts` localizes MCP error/validation messages (auth rejection, tenant
76
+ refusal, tool input-validation) per `mcp-standard.md`'s bilingual mandate
77
+ (§Bilingual): the client sends `X-MCP-Locale: fr` or `X-MCP-Locale: en`
78
+ (default `en` when absent), and `src/httpApp.ts` + `src/mcpServerFactory.ts`
79
+ route the corresponding message through `t(locale, key)`.
80
+
81
+ The two resource files (`src/locales/{en,fr}.json`) follow the exact
82
+ namespaced-resource convention shipped by `@vantageos/mosaic-i18n@0.1.1`
83
+ (`node_modules/@vantageos/mosaic-i18n/src/locales/{en,fr}.json`): a flat
84
+ `<Namespace>.<category>.<key>` -> string object, FR and EN kept key-for-key
85
+ in lockstep. `mosaic-i18n` itself is a React/`react-i18next` package and
86
+ cannot be called directly from a JSON-RPC server, so its runtime API is not
87
+ imported here — its resource-file shape is reused instead, under this
88
+ boilerplate's own `Mcp` namespace.
89
+
90
+ ## Testing
91
+
92
+ ```bash
93
+ npm test
94
+ ```
95
+
96
+ Runs both transports against the real official-SDK code paths: the HTTP
97
+ suite drives the Hono app in-process; the stdio suite spawns the built
98
+ `dist/stdio.js` binary as a real child process via `StdioClientTransport`.
99
+
100
+ ## Adding a tool
101
+
102
+ Register it once in `src/mcpServerFactory.ts` — it is then available on
103
+ both transports automatically. There is no separate REST route to add.
104
+
105
+ ## Packaging & distribution
106
+
107
+ This boilerplate is itself a publishable/clonable npm package. Two artifacts
108
+ carry its distribution identity:
109
+
110
+ - `package.json` — the npm package. `files` ships only the built `dist/` tree
111
+ (compiled server + `dist/locales/` + `dist/ui-bundle/widget.html`), the
112
+ `README.md`, the `LICENSE`, the plugin manifest, and the example client
113
+ config — never `src/`, tests, or tooling. `main`/`types`/`exports` point at
114
+ `dist/index.js`; the `mcp-boilerplate` bin points at `dist/stdio.js`.
115
+ `publishConfig.access = public` is required for the `@vantageos` scope.
116
+ - `.claude-plugin/plugin.json` — the Claude Code marketplace / VantageRegistry
117
+ catalogue manifest, following the fleet MCP-server convention (mirrors
118
+ `vantage-frameworks-mcp` / `vantage-agent-composer-mcp`): `name`, `version`,
119
+ bilingual `description` + `description_fr`, `author`, `license`, `homepage`,
120
+ `category: "mcp-server"`, `bilingual: true`, `keywords`.
121
+
122
+ ### Scaffold → build → publish
123
+
124
+ ```bash
125
+ # 1. Scaffold: clone this repo (or the npm tarball) as your server's skeleton.
126
+ git clone https://github.com/elpiarthera/vantage-mcp-boilerplate.git my-mcp
127
+ cd my-mcp && npm install --legacy-peer-deps # see dev peer-range note above
128
+
129
+ # 2. Rename: set package.json "name"/"bin" and .claude-plugin/plugin.json
130
+ # "name" to your server, then add tools in src/mcpServerFactory.ts.
131
+
132
+ # 3. Build + verify from a clean tree (what CI and prepublishOnly enforce):
133
+ rm -rf dist node_modules/.vite && npm run build && npm test && npm run evals
134
+
135
+ # 4. Inspect the tarball before publishing — confirm dist/ ships and nothing leaks:
136
+ npm pack --dry-run
137
+
138
+ # 5. Publish to the two channels:
139
+ npm publish # npm registry (prepublishOnly re-runs build+test+evals)
140
+ # VantageRegistry catalogue: register .claude-plugin/plugin.json via
141
+ # the fleet create-plugin / generate-marketplace skill (VR upsert).
142
+ ```
143
+
144
+ `prepublishOnly` re-runs `build → build:ui → test → evals` so a broken or
145
+ src-only tarball can never reach npm. The distribution channels are **npm**
146
+ (the runnable package) and the **VantageRegistry / Claude Code marketplace
147
+ catalogue** (discovery, via `plugin.json`).
148
+
149
+ ## MCP Apps UI layer
150
+
151
+ `render_items_ui` renders `EXAMPLE_ITEMS` as an interactive widget via the
152
+ official `@modelcontextprotocol/ext-apps/server` (`registerAppTool` +
153
+ `registerAppResource`), GA `_meta.ui.resourceUri` nested form (stable
154
+ 2026-01-26) — never the deprecated flat `_meta["ui/resourceUri"]` key. A
155
+ markdown fallback is always present in `content[0]`, even for clients that
156
+ never negotiate the `io.modelcontextprotocol/ui` extension.
157
+
158
+ The widget (`src/ui/`) reuses `@vantageos/mosaic-blocks`' `MosaicCard`
159
+ family and is bundled to a single self-contained HTML file
160
+ (`vite-plugin-singlefile`, `dist/ui-bundle/widget.html`, ~70 KB gzipped, no
161
+ external runtime fetch). `npm run build:ui` produces this bundle
162
+ independently of `npm run build` (tsc), on a non-colliding output path
163
+ (`dist/ui-bundle/` vs tsc's `dist/ui/`) — the two build steps write to the
164
+ same-looking `dist/ui*` tree but never share a directory, so
165
+ `emptyOutDir` on the vite side can never delete a tsc-compiled server
166
+ module.
167
+
168
+ **Known dev peer-range skew**: `@vantageos/mosaic-blocks@0.5.34-alpha`
169
+ declares `peerOptional @vantageos/cloud-identity@^0.2`, while this
170
+ boilerplate's own dependency is `^0.3.0` (via `@vantageos/mcp-auth-kit`).
171
+ `npm install` currently needs `--legacy-peer-deps` to resolve. This is a
172
+ dev-only peer-range mismatch (mosaic-blocks is a devDependency, used only
173
+ to build the widget bundle) — the full test suite is green from a clean
174
+ build under the `^0.3.0` runtime dependency, proving no runtime conflict.
175
+
176
+ **Publish ordering (hard requirement)**: this boilerplate depends on
177
+ `@vantageos/mcp-protocol-kit@^0.2.0`. The `tools/list` wiring uses the kit's
178
+ `buildToolsListResult`, which exists ONLY in kit 0.2.0+. In 0.x semver `^0.2.0`
179
+ does NOT accept 0.1.x, so a fresh `npm install` from the registry resolves the
180
+ kit build the code actually needs. Therefore **kit `@vantageos/mcp-protocol-kit`
181
+ 0.2.0 MUST be published to npm BEFORE this boilerplate can be installed or
182
+ published from the registry.** This is a strict ordering, not optional: publish
183
+ the kit first, confirm 0.2.0 is live at the registry, then the boilerplate.
package/dist/auth.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * OAuth 2.1 resource-server wiring for this MCP server's HTTP transport.
3
+ *
4
+ * Reuses @vantageos/mcp-auth-kit's `verifyMcpBearerRequest` facade
5
+ * unmodified — this module never hand-rolls JWT verification. It only
6
+ * supplies configuration (issuer, audience, JWKS resolver, resource
7
+ * metadata URL) and adapts the facade's result to a plain
8
+ * accept/reject decision for the HTTP transport.
9
+ *
10
+ * This is the "OAuth 2.1" tier of the two auth tiers this boilerplate
11
+ * supports (see config.ts `AuthMode`). The other tier — "public" — never
12
+ * touches this module at all: the HTTP gate is skipped entirely, and the
13
+ * stdio transport (stdio.ts) never had a gate to begin with (local process
14
+ * trust model, per mcp-standard.md).
15
+ */
16
+ import { type McpResourceServerResult } from "@vantageos/mcp-auth-kit";
17
+ import type { JWTVerifyGetKey } from "jose";
18
+ export interface AuthConfig {
19
+ realm: string;
20
+ resourceMetadataUrl: string;
21
+ jwks: JWTVerifyGetKey;
22
+ issuer: string;
23
+ audience: string;
24
+ authorizationServers: string[];
25
+ }
26
+ export declare function authenticateRequest(authHeader: string | undefined, config: AuthConfig): Promise<McpResourceServerResult>;
27
+ export declare function protectedResourceMetadataDocument(config: AuthConfig): import("@vantageos/mcp-auth-kit").ProtectedResourceMetadataDocument;
package/dist/auth.js ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * OAuth 2.1 resource-server wiring for this MCP server's HTTP transport.
3
+ *
4
+ * Reuses @vantageos/mcp-auth-kit's `verifyMcpBearerRequest` facade
5
+ * unmodified — this module never hand-rolls JWT verification. It only
6
+ * supplies configuration (issuer, audience, JWKS resolver, resource
7
+ * metadata URL) and adapts the facade's result to a plain
8
+ * accept/reject decision for the HTTP transport.
9
+ *
10
+ * This is the "OAuth 2.1" tier of the two auth tiers this boilerplate
11
+ * supports (see config.ts `AuthMode`). The other tier — "public" — never
12
+ * touches this module at all: the HTTP gate is skipped entirely, and the
13
+ * stdio transport (stdio.ts) never had a gate to begin with (local process
14
+ * trust model, per mcp-standard.md).
15
+ */
16
+ import { buildProtectedResourceMetadata, verifyMcpBearerRequest, } from "@vantageos/mcp-auth-kit";
17
+ export async function authenticateRequest(authHeader, config) {
18
+ return verifyMcpBearerRequest(authHeader, {
19
+ realm: config.realm,
20
+ resourceMetadataUrl: config.resourceMetadataUrl,
21
+ jwks: config.jwks,
22
+ issuer: config.issuer,
23
+ audience: config.audience,
24
+ });
25
+ }
26
+ export function protectedResourceMetadataDocument(config) {
27
+ return buildProtectedResourceMetadata({
28
+ resource: config.resourceMetadataUrl.replace(/\/\.well-known\/.*/, ""),
29
+ authorizationServers: config.authorizationServers,
30
+ });
31
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Environment-derived configuration for the two auth tiers this
3
+ * boilerplate ships (mcp-standard.md "Authentication & Authorization"):
4
+ *
5
+ * - "oauth" (default): OAuth 2.1 resource server on the HTTP transport —
6
+ * every /mcp call requires a valid RS256 JWT, audience/issuer-bound.
7
+ * - "public": no gate at all on the HTTP transport — for local/self-host
8
+ * use where the caller is already trusted (mirrors the stdio transport's
9
+ * trust model, which never had a gate). Chosen explicitly, never a
10
+ * silent fallback from a missing env var.
11
+ *
12
+ * No hardcoded business values — every deployment-specific identity (base
13
+ * URL, issuer, audience) comes from the environment, never a literal in
14
+ * code.
15
+ */
16
+ import { type JWTVerifyGetKey, createLocalJWKSet } from "jose";
17
+ import type { AuthConfig } from "./auth.js";
18
+ export type AuthMode = "oauth" | "public";
19
+ export declare function loadAuthMode(env?: NodeJS.ProcessEnv): AuthMode;
20
+ export declare function loadAuthConfig(env?: NodeJS.ProcessEnv, jwksOverride?: JWTVerifyGetKey): AuthConfig;
21
+ export { createLocalJWKSet };
package/dist/config.js ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Environment-derived configuration for the two auth tiers this
3
+ * boilerplate ships (mcp-standard.md "Authentication & Authorization"):
4
+ *
5
+ * - "oauth" (default): OAuth 2.1 resource server on the HTTP transport —
6
+ * every /mcp call requires a valid RS256 JWT, audience/issuer-bound.
7
+ * - "public": no gate at all on the HTTP transport — for local/self-host
8
+ * use where the caller is already trusted (mirrors the stdio transport's
9
+ * trust model, which never had a gate). Chosen explicitly, never a
10
+ * silent fallback from a missing env var.
11
+ *
12
+ * No hardcoded business values — every deployment-specific identity (base
13
+ * URL, issuer, audience) comes from the environment, never a literal in
14
+ * code.
15
+ */
16
+ import { createLocalJWKSet, createRemoteJWKSet } from "jose";
17
+ export function loadAuthMode(env = process.env) {
18
+ const raw = (env.AUTH_MODE ?? "oauth").toLowerCase();
19
+ if (raw !== "oauth" && raw !== "public") {
20
+ throw new Error(`AUTH_MODE must be "oauth" or "public", got "${raw}". Set AUTH_MODE=public for the no-auth local/self-host tier, or leave unset for the OAuth 2.1 default.`);
21
+ }
22
+ return raw;
23
+ }
24
+ export function loadAuthConfig(env = process.env, jwksOverride) {
25
+ const issuer = env.MCP_OAUTH_ISSUER;
26
+ const audience = env.MCP_OAUTH_AUDIENCE;
27
+ const publicBaseUrl = env.PUBLIC_BASE_URL ?? "http://localhost:8787";
28
+ if (!issuer || !audience) {
29
+ throw new Error("MCP_OAUTH_ISSUER and MCP_OAUTH_AUDIENCE are required to run the HTTP transport in " +
30
+ "oauth mode (OAuth 2.1 resource-server requirement — mcp-standard.md Critical Rule #14). " +
31
+ "Set AUTH_MODE=public instead to run the no-auth local/self-host tier.");
32
+ }
33
+ const jwks = jwksOverride ?? createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`));
34
+ return {
35
+ realm: "mcp",
36
+ resourceMetadataUrl: `${publicBaseUrl}/.well-known/oauth-protected-resource`,
37
+ jwks,
38
+ issuer,
39
+ audience,
40
+ authorizationServers: [issuer],
41
+ };
42
+ }
43
+ export { createLocalJWKSet };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Seed data for the `list_items` multi-tenant example tool. `createdBy` and
3
+ * `namespace` follow the tenant-id convention documented in tenancy.ts
4
+ * (`scopeForTenant`) — each row is owned by, and namespaced under, exactly
5
+ * one tenant.
6
+ */
7
+ export interface ExampleItem {
8
+ id: string;
9
+ name: string;
10
+ createdBy: string;
11
+ namespace: string;
12
+ }
13
+ export declare const EXAMPLE_ITEMS: ExampleItem[];
@@ -0,0 +1,20 @@
1
+ export const EXAMPLE_ITEMS = [
2
+ {
3
+ id: "item-1",
4
+ name: "Tenant A widget",
5
+ createdBy: "org_a",
6
+ namespace: "org_a",
7
+ },
8
+ {
9
+ id: "item-2",
10
+ name: "Tenant A gadget",
11
+ createdBy: "org_a",
12
+ namespace: "org_a",
13
+ },
14
+ {
15
+ id: "item-3",
16
+ name: "Tenant B widget",
17
+ createdBy: "org_b",
18
+ namespace: "org_b",
19
+ },
20
+ ];
package/dist/http.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/http.js ADDED
@@ -0,0 +1,28 @@
1
+ import { loadAuthConfig, loadAuthMode } from "./config.js";
2
+ import { buildHttpApp } from "./httpApp.js";
3
+ const authMode = loadAuthMode();
4
+ const app = buildHttpApp(authMode === "oauth" ? { authMode, auth: loadAuthConfig() } : { authMode });
5
+ const port = Number(process.env.PORT ?? 8787);
6
+ // Hono's fetch handler adapted onto Node's http server (no extra framework
7
+ // dependency beyond hono + node:http).
8
+ const { createServer } = await import("node:http");
9
+ createServer(async (req, res) => {
10
+ const url = `http://${req.headers.host ?? "localhost"}${req.url}`;
11
+ const chunks = [];
12
+ for await (const chunk of req)
13
+ chunks.push(chunk);
14
+ const body = req.method === "GET" || req.method === "HEAD" ? undefined : Buffer.concat(chunks);
15
+ const headers = new Headers();
16
+ for (const [key, value] of Object.entries(req.headers)) {
17
+ if (typeof value === "string")
18
+ headers.set(key, value);
19
+ }
20
+ const request = new Request(url, { method: req.method, headers, body });
21
+ const response = await app.fetch(request);
22
+ res.statusCode = response.status;
23
+ response.headers.forEach((value, key) => res.setHeader(key, value));
24
+ const buf = Buffer.from(await response.arrayBuffer());
25
+ res.end(buf);
26
+ }).listen(port, () => {
27
+ console.log(`mcp-boilerplate HTTP transport listening on :${port} (authMode: ${authMode})`);
28
+ });
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Hono HTTP app: stateless MCP transport for the boilerplate skeleton.
3
+ *
4
+ * The MCP wire protocol itself is served via the official
5
+ * `@modelcontextprotocol/sdk` `WebStandardStreamableHTTPServerTransport`
6
+ * (stateless mode: `sessionIdGenerator: undefined`) — a standard MCP client
7
+ * (Claude Code, ChatGPT, Cursor) speaks JSON-RPC over this endpoint exactly
8
+ * as it would over any other MCP HTTP server. No ad-hoc REST routes stand
9
+ * in front of tool calls.
10
+ *
11
+ * True statelessness: a FRESH `McpServer` + a FRESH stateless transport are
12
+ * created for every single POST to /mcp, connected, used to handle exactly
13
+ * one request, and never retained. Nothing survives past the request/
14
+ * response cycle, so the endpoint is horizontally load-balanceable and two
15
+ * concurrent requests never share any in-memory object.
16
+ *
17
+ * `server/discover` is NOT a separate REST route: it is a JSON-RPC method
18
+ * on this same `/mcp` endpoint (registered on the low-level `Server` in
19
+ * mcpServerFactory.ts via `@vantageos/mcp-protocol-kit`'s
20
+ * `handleServerDiscover`), exactly like `tools/list` and `tools/call` —
21
+ * a standard MCP client dispatches all three the same way.
22
+ *
23
+ * Auth tiers (mcp-standard.md "Authentication & Authorization"):
24
+ *
25
+ * - `authMode: "oauth"` — OAuth 2.1 resource server via
26
+ * @vantageos/mcp-auth-kit. Every /mcp call requires a valid Bearer JWT
27
+ * (RS256, audience/issuer-bound); the RFC 9728 Protected Resource
28
+ * Metadata document is served at `/.well-known/oauth-protected-resource`;
29
+ * `/health` stays token-free. Requires an `AuthConfig` (deps.auth) — see
30
+ * config.ts `loadAuthConfig()`.
31
+ * - `authMode: "public"` — no gate at all. For local/self-host use where
32
+ * the caller is already trusted, mirroring the stdio transport's trust
33
+ * model (stdio.ts never had a gate to begin with). This is the DEFAULT
34
+ * when `buildHttpApp()` is called with no deps, so the transport-only
35
+ * tests (httpHandshake.test.ts, protocolKitWiring.test.ts) exercise the
36
+ * skeleton without needing OAuth configuration.
37
+ */
38
+ import { Hono } from "hono";
39
+ import { type AuthConfig } from "./auth.js";
40
+ import type { AuthMode } from "./config.js";
41
+ import { type ResolvedTenant } from "./tenancy.js";
42
+ export interface AppDeps {
43
+ authMode?: AuthMode;
44
+ auth?: AuthConfig;
45
+ }
46
+ type Variables = {
47
+ tenant?: ResolvedTenant;
48
+ };
49
+ export declare function buildHttpApp(deps?: AppDeps): Hono<{
50
+ Variables: Variables;
51
+ }>;
52
+ export {};
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Hono HTTP app: stateless MCP transport for the boilerplate skeleton.
3
+ *
4
+ * The MCP wire protocol itself is served via the official
5
+ * `@modelcontextprotocol/sdk` `WebStandardStreamableHTTPServerTransport`
6
+ * (stateless mode: `sessionIdGenerator: undefined`) — a standard MCP client
7
+ * (Claude Code, ChatGPT, Cursor) speaks JSON-RPC over this endpoint exactly
8
+ * as it would over any other MCP HTTP server. No ad-hoc REST routes stand
9
+ * in front of tool calls.
10
+ *
11
+ * True statelessness: a FRESH `McpServer` + a FRESH stateless transport are
12
+ * created for every single POST to /mcp, connected, used to handle exactly
13
+ * one request, and never retained. Nothing survives past the request/
14
+ * response cycle, so the endpoint is horizontally load-balanceable and two
15
+ * concurrent requests never share any in-memory object.
16
+ *
17
+ * `server/discover` is NOT a separate REST route: it is a JSON-RPC method
18
+ * on this same `/mcp` endpoint (registered on the low-level `Server` in
19
+ * mcpServerFactory.ts via `@vantageos/mcp-protocol-kit`'s
20
+ * `handleServerDiscover`), exactly like `tools/list` and `tools/call` —
21
+ * a standard MCP client dispatches all three the same way.
22
+ *
23
+ * Auth tiers (mcp-standard.md "Authentication & Authorization"):
24
+ *
25
+ * - `authMode: "oauth"` — OAuth 2.1 resource server via
26
+ * @vantageos/mcp-auth-kit. Every /mcp call requires a valid Bearer JWT
27
+ * (RS256, audience/issuer-bound); the RFC 9728 Protected Resource
28
+ * Metadata document is served at `/.well-known/oauth-protected-resource`;
29
+ * `/health` stays token-free. Requires an `AuthConfig` (deps.auth) — see
30
+ * config.ts `loadAuthConfig()`.
31
+ * - `authMode: "public"` — no gate at all. For local/self-host use where
32
+ * the caller is already trusted, mirroring the stdio transport's trust
33
+ * model (stdio.ts never had a gate to begin with). This is the DEFAULT
34
+ * when `buildHttpApp()` is called with no deps, so the transport-only
35
+ * tests (httpHandshake.test.ts, protocolKitWiring.test.ts) exercise the
36
+ * skeleton without needing OAuth configuration.
37
+ */
38
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
39
+ import { Hono } from "hono";
40
+ import { authenticateRequest, protectedResourceMetadataDocument } from "./auth.js";
41
+ import { detectLocale, t } from "./i18n.js";
42
+ import { createBoilerplateMcpServer } from "./mcpServerFactory.js";
43
+ import { resolveTenantFromJwtPayload } from "./tenancy.js";
44
+ const WELL_KNOWN_PATHS = ["/.well-known/oauth-protected-resource", "/health"];
45
+ export function buildHttpApp(deps = {}) {
46
+ const app = new Hono();
47
+ const authMode = deps.authMode ?? "public";
48
+ app.get("/health", (c) => c.json({ status: "ok" }));
49
+ if (authMode === "oauth") {
50
+ if (!deps.auth) {
51
+ throw new Error("authMode 'oauth' requires an AuthConfig (deps.auth) — build one via " +
52
+ "config.ts loadAuthConfig(), or pass authMode: 'public' for the no-auth tier.");
53
+ }
54
+ const auth = deps.auth;
55
+ app.get("/.well-known/oauth-protected-resource", (c) => {
56
+ return c.json(protectedResourceMetadataDocument(auth));
57
+ });
58
+ // OAuth 2.1 resource-server gate — every route below this line requires
59
+ // a valid Bearer JWT, verified entirely by @vantageos/mcp-auth-kit.
60
+ app.use("*", async (c, next) => {
61
+ if (WELL_KNOWN_PATHS.includes(c.req.path)) {
62
+ await next();
63
+ return;
64
+ }
65
+ const authHeader = c.req.header("Authorization");
66
+ const result = await authenticateRequest(authHeader, auth);
67
+ if (!result.ok) {
68
+ const locale = detectLocale(c.req.header("X-MCP-Locale"));
69
+ c.header("WWW-Authenticate", result.headers["WWW-Authenticate"]);
70
+ return c.json({ error: t(locale, "auth.unauthorized") }, result.status);
71
+ }
72
+ // Tenant resolution (tenancy.ts) is attempted here but is NOT
73
+ // request-fatal: `initialize`/`tools/list` and any tenant-agnostic
74
+ // tool (e.g. `ping`) must keep working for a verified caller that has
75
+ // no organization on their token. Tenant-scoped tools (`list_items`)
76
+ // enforce the requirement themselves, at the tool-call level (see
77
+ // mcpServerFactory.ts) — a missing tenant there is refused as an MCP
78
+ // tool error, never silently treated as an unscoped/master caller.
79
+ try {
80
+ c.set("tenant", resolveTenantFromJwtPayload(result.payload));
81
+ }
82
+ catch {
83
+ // No organization on this token — tenant stays unset. Tenant-scoped
84
+ // tools refuse explicitly when they see no tenant under oauth mode.
85
+ }
86
+ await next();
87
+ });
88
+ }
89
+ // The one real MCP endpoint: standard JSON-RPC over Streamable HTTP,
90
+ // stateless mode. GET/POST/DELETE are all delegated to the SDK transport,
91
+ // which handles the protocol's method dispatch itself.
92
+ app.all("/mcp", async (c) => {
93
+ const tenant = authMode === "oauth" ? c.get("tenant") : undefined;
94
+ const locale = detectLocale(c.req.header("X-MCP-Locale"));
95
+ const server = createBoilerplateMcpServer({
96
+ tenant,
97
+ requireTenant: authMode === "oauth",
98
+ locale,
99
+ });
100
+ const transport = new WebStandardStreamableHTTPServerTransport({
101
+ sessionIdGenerator: undefined, // stateless mode — no session ID anywhere
102
+ enableJsonResponse: true,
103
+ });
104
+ await server.connect(transport);
105
+ try {
106
+ return await transport.handleRequest(c.req.raw);
107
+ }
108
+ finally {
109
+ await transport.close();
110
+ }
111
+ });
112
+ return app;
113
+ }
package/dist/i18n.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Thin server-side locale layer for MCP error/validation messages.
3
+ *
4
+ * REUSE: @vantageos/mosaic-i18n@0.1.1 is a React/react-i18next package
5
+ * (`peerDependencies: i18next, react-i18next`) — its runtime API targets a
6
+ * React component tree, not a JSON-RPC server, so it cannot be called
7
+ * directly from tool handlers here. What IS reused, verified firsthand in
8
+ * `node_modules/@vantageos/mosaic-i18n/src/locales/{en,fr}.json`, is its
9
+ * resource-file CONVENTION: a flat namespaced object
10
+ * (`<Component>.<category>.<key>` -> string), one file per locale, FR and
11
+ * EN kept in lockstep key-for-key. `src/locales/{en,fr}.json` in this
12
+ * boilerplate follows that exact shape under a single `Mcp` namespace
13
+ * (`Mcp.auth.*`, `Mcp.tenant.*`, `Mcp.validation.*`), so the two locale
14
+ * files could be merged into mosaic-i18n's own resource files verbatim if
15
+ * this boilerplate ever grows a React UI consumer of the same strings.
16
+ *
17
+ * Locale detection follows mcp-standard.md's `X-MCP-Locale` request header
18
+ * (§Bilingual, l.234): "fr" selects the French resource, anything else
19
+ * (including a missing header) defaults to English.
20
+ */
21
+ export type McpLocale = "en" | "fr";
22
+ /**
23
+ * Resolves the caller's locale from the `X-MCP-Locale` request header.
24
+ * Defaults to "en" for a missing header or any value other than "fr".
25
+ */
26
+ export declare function detectLocale(headerValue: string | null | undefined): McpLocale;
27
+ /**
28
+ * Looks up a translated MCP message. `key` is a dot path under the `Mcp`
29
+ * namespace, e.g. `"auth.unauthorized"` or `"tenant.permissionDenied"`.
30
+ * Falls back to the English string (never to the raw key) if the requested
31
+ * locale is somehow missing a key the English resource carries.
32
+ */
33
+ export declare function t(locale: McpLocale, key: string): string;