@vantageos/mcp-boilerplate 0.1.2 → 0.2.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 +16 -8
- package/.mcp.json +8 -0
- package/CHANGELOG.md +55 -1
- package/README.fr.md +177 -142
- package/README.md +170 -133
- package/dist/auth.d.ts +20 -10
- package/dist/auth.js +9 -8
- package/dist/config.d.ts +10 -3
- package/dist/config.js +31 -3
- package/dist/exampleItems.d.ts +5 -4
- package/dist/exampleItems.js +3 -0
- package/dist/http.js +11 -1
- package/dist/httpApp.d.ts +23 -30
- package/dist/httpApp.js +73 -74
- package/dist/i18n.d.ts +6 -0
- package/dist/i18n.js +9 -0
- package/dist/locales/en.json +9 -0
- package/dist/locales/fr.json +9 -0
- package/dist/mcpServerFactory.d.ts +42 -41
- package/dist/mcpServerFactory.js +114 -165
- package/dist/oauth/authorizationServer.d.ts +56 -0
- package/dist/oauth/authorizationServer.js +423 -0
- package/dist/oauth/clerk.d.ts +15 -0
- package/dist/oauth/clerk.js +59 -0
- package/dist/oauth/clientRegistry.d.ts +50 -0
- package/dist/oauth/clientRegistry.js +203 -0
- package/dist/oauth/codeStore.d.ts +11 -0
- package/dist/oauth/codeStore.js +28 -0
- package/dist/oauth/picker.d.ts +11 -0
- package/dist/oauth/picker.js +67 -0
- package/dist/oauth/settings.d.ts +48 -0
- package/dist/oauth/settings.js +100 -0
- package/dist/oauth/tokenIssuer.d.ts +22 -0
- package/dist/oauth/tokenIssuer.js +61 -0
- package/dist/stdio.d.ts +7 -6
- package/dist/stdio.js +20 -19
- package/dist/tenancy.d.ts +16 -21
- package/dist/tenancy.js +12 -26
- package/dist/ui/uiResource.d.ts +36 -3
- package/dist/ui/uiResource.js +40 -6
- package/dist/version.d.ts +2 -0
- package/dist/version.js +8 -0
- package/docs/architecture.md +82 -0
- package/docs/oauth-stores.md +60 -0
- package/examples/oauth.env.example +25 -0
- package/mcp.json +19 -0
- package/package.json +22 -13
- package/plugin.json +43 -0
- package/scripts/plugin-manifest.source.json +35 -0
- package/skills/connect-mcp-server/SKILL.md +40 -0
package/README.md
CHANGED
|
@@ -1,93 +1,141 @@
|
|
|
1
1
|
# @vantageos/mcp-boilerplate
|
|
2
2
|
|
|
3
|
-
SELLABLE AS: `mcp-boilerplate` — a clonable MCP server starter for the Claude Code marketplace / VantageRegistry catalogue, published as `@vantageos/mcp-boilerplate` on npm.
|
|
3
|
+
SELLABLE AS: `mcp-boilerplate` — a clonable MCP server starter for the Claude Code marketplace / VantageRegistry catalogue and for ChatGPT, published as `@vantageos/mcp-boilerplate` on npm.
|
|
4
4
|
|
|
5
|
-
A clonable MCP server skeleton
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
A clonable MCP server skeleton on the official
|
|
6
|
+
[`@modelcontextprotocol/server`](https://www.npmjs.com/package/@modelcontextprotocol/server)
|
|
7
|
+
SDK v2: protocol **2026-07-28** with the 2025 handshake still served, stdio and
|
|
8
|
+
stateless Streamable HTTP from one server definition, its **own OAuth 2.1
|
|
9
|
+
authorization server** (a Clerk person picks an organisation), an MCP Apps UI
|
|
10
|
+
and FR/EN messages. No custom REST route ever stands in front of a tool call.
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
transports. Later layers (protocol-kit result types, auth, multi-tenant
|
|
13
|
-
scoping, UI extensions, i18n) build on top of this bootstrap without
|
|
14
|
-
changing the transport wiring.
|
|
12
|
+
It ships three small example tools (`ping`, `list_items`, `render_items_ui`)
|
|
13
|
+
that exercise every layer. Replace them with yours; the wiring stays.
|
|
15
14
|
|
|
16
15
|
## Structure
|
|
17
16
|
|
|
18
17
|
```
|
|
19
18
|
src/
|
|
20
19
|
mcpServerFactory.ts # single MCP server definition, shared by both transports
|
|
21
|
-
|
|
20
|
+
version.ts # server name + version, derived from package.json
|
|
21
|
+
stdio.ts # serveStdio(factory): both protocol eras over stdio
|
|
22
22
|
http.ts # Node http.Server adapter around the Hono app
|
|
23
|
-
httpApp.ts #
|
|
24
|
-
auth.ts #
|
|
25
|
-
config.ts # env-derived AuthMode + AuthConfig
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
23
|
+
httpApp.ts # createMcpHandler(factory) on /mcp + the OAuth gate
|
|
24
|
+
auth.ts # resource-server wiring (@vantageos/cloud-identity)
|
|
25
|
+
config.ts # env-derived AuthMode + external-issuer AuthConfig
|
|
26
|
+
tenancy.ts # verified JWT -> tenant; rows filtered by stored orgId
|
|
27
|
+
oauth/ # own authorization server (cloud-identity decides)
|
|
28
|
+
authorizationServer.ts settings.ts clerk.ts tokenIssuer.ts
|
|
29
|
+
clientRegistry.ts codeStore.ts picker.ts
|
|
30
|
+
ui/ # MCP Apps widget + ui:// resource
|
|
31
|
+
skills/connect-mcp-server/SKILL.md # onboarding skill (English)
|
|
32
|
+
scripts/
|
|
33
|
+
plugin-manifest.source.json # the ONE source of every packaging manifest
|
|
34
|
+
sync-plugin-version.mjs # generates the four manifests (runs in `npm run build`)
|
|
35
|
+
generate-signing-key.mjs # prints an RS256 private JWK for MCP_OAUTH_SIGNING_JWK
|
|
36
|
+
tests/ evals/ docs/
|
|
35
37
|
```
|
|
36
38
|
|
|
37
39
|
## Quick start
|
|
38
40
|
|
|
39
41
|
```bash
|
|
40
42
|
npm install
|
|
41
|
-
npm run build
|
|
43
|
+
npm run build && npm run build:ui
|
|
42
44
|
|
|
43
45
|
# stdio (local client config, e.g. Claude Code / Cursor) — never gated,
|
|
44
46
|
# the local process invoking stdio IS the caller.
|
|
45
47
|
npm run start:stdio
|
|
46
48
|
|
|
47
|
-
# Streamable HTTP
|
|
48
|
-
MCP_OAUTH_ISSUER=https://your-tenant.example.com \
|
|
49
|
-
MCP_OAUTH_AUDIENCE=https://your-mcp-server.example.com \
|
|
50
|
-
PORT=8787 npm run start:http
|
|
51
|
-
|
|
52
|
-
# Streamable HTTP, no-auth "Public MCP" tier (local/self-host, trusted caller):
|
|
49
|
+
# Streamable HTTP, public tier (local/self-host, trusted caller):
|
|
53
50
|
AUTH_MODE=public PORT=8787 npm run start:http
|
|
51
|
+
|
|
52
|
+
# Streamable HTTP, OAuth tier (default): this server is its own authorization
|
|
53
|
+
# server. See examples/oauth.env.example for every required variable.
|
|
54
|
+
node scripts/generate-signing-key.mjs # -> MCP_OAUTH_SIGNING_JWK
|
|
55
|
+
PORT=8787 npm run start:http
|
|
54
56
|
```
|
|
55
57
|
|
|
56
|
-
##
|
|
58
|
+
## Protocol and transports
|
|
59
|
+
|
|
60
|
+
One factory (`createBoilerplateMcpServer`) builds a fresh `McpServer` per
|
|
61
|
+
request (HTTP) or per connection (stdio). The SDK owns the era decision:
|
|
57
62
|
|
|
58
|
-
|
|
63
|
+
- a **2026-07-28** client opens with `server/discover`; `tools/list`,
|
|
64
|
+
`server/discover` and `resources/*` results carry `resultType`, `ttlMs` and
|
|
65
|
+
`cacheScope` from the SDK's `cacheHints` (set once in the factory);
|
|
66
|
+
- a **2025-era** client opens with `initialize` and is served through the SDK's
|
|
67
|
+
stateless legacy leg on HTTP, or a pinned connection on stdio.
|
|
68
|
+
|
|
69
|
+
The 2025 floor is **2025-06-18**: the SDK's `supportedProtocolVersions` option
|
|
70
|
+
lists only `2025-11-25` and `2025-06-18`, so an older `initialize` is
|
|
71
|
+
counter-offered `2025-11-25`, never served at its own version. The advertised
|
|
72
|
+
server version is read from `package.json`.
|
|
73
|
+
|
|
74
|
+
## Tool records
|
|
75
|
+
|
|
76
|
+
Every tool declares `title`, the four annotations as explicit booleans
|
|
77
|
+
(`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`), a typed
|
|
78
|
+
input schema and a typed output schema (Zod 4), and returns `structuredContent`
|
|
79
|
+
plus a text block with the same JSON. `tests/toolRecordComplete.test.ts` walks
|
|
80
|
+
`tools/list` and fails on any gap. Annotations are untrusted hints for the
|
|
81
|
+
client (mcp-standard C-15); never gate an action on them alone.
|
|
82
|
+
|
|
83
|
+
## Authentication tiers
|
|
59
84
|
|
|
60
85
|
| `AUTH_MODE` | Behavior |
|
|
61
86
|
|---|---|
|
|
62
|
-
| `oauth` (default) | OAuth 2.1 resource server (
|
|
63
|
-
| `public` | No gate
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
`
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
`
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
| `oauth` (default) | OAuth 2.1. Without `MCP_OAUTH_ISSUER`, this server is both the authorization server and the resource server (below). With `MCP_OAUTH_ISSUER` + `MCP_OAUTH_AUDIENCE`, it is a resource server only, for an external issuer. Missing or invalid token -> `401` + `WWW-Authenticate` pointing at the PRM. |
|
|
88
|
+
| `public` | No gate on HTTP — local/self-host, trusted caller. No authorization-server document is served. |
|
|
89
|
+
|
|
90
|
+
stdio is never gated in either mode.
|
|
91
|
+
|
|
92
|
+
### Own authorization server (ruling R1)
|
|
93
|
+
|
|
94
|
+
| Route | Role |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `GET /.well-known/oauth-authorization-server` (+ `openid-configuration`) | RFC 8414 metadata: S256 only, RFC 9207 `iss`, CIMD and DCR advertised |
|
|
97
|
+
| `GET /.well-known/oauth-protected-resource/mcp` | RFC 9728 PRM (`resource` = `<base>/mcp`) via cloud-identity |
|
|
98
|
+
| `GET /.well-known/jwks.json` | public signing keys only |
|
|
99
|
+
| `POST /register` | Dynamic Client Registration fallback (public clients, PKCE) |
|
|
100
|
+
| `GET /authorize`, `GET /authorize/callback`, `POST /authorize/org` | Clerk sign-in, then the person + organisation picker with consent |
|
|
101
|
+
| `POST /token` | `authorization_code` + PKCE S256 -> RS256 JWT bound to the resource, the issuer and the chosen organisation |
|
|
102
|
+
| `GET /userinfo` | OIDC subject (and email when the scope allows) |
|
|
103
|
+
|
|
104
|
+
Every decision — redirect/PKCE/resource/scope checks, the consent token, code
|
|
105
|
+
binding and redemption, discovery and userinfo shapes — is made by
|
|
106
|
+
`@vantageos/cloud-identity` (`startAuthorize`, `resumeAuthorize`,
|
|
107
|
+
`exchangeAuthorizationCode`, `buildDiscoveryDocument`, `buildUserInfo`). This app
|
|
108
|
+
supplies the environment, the stores, the Clerk calls, the picker page and the
|
|
109
|
+
signature. A client may identify itself by a Client ID Metadata Document URL
|
|
110
|
+
(fetched SSRF-guarded) or register through `/register`.
|
|
111
|
+
|
|
112
|
+
**Fail-closed**: a missing variable (`PUBLIC_BASE_URL`, `CLERK_DOMAIN`,
|
|
113
|
+
`AUTHORIZE_SIGN_IN_URL`, `AUTHORIZE_CALLBACK_URL`, `AUTHORIZE_STATE_SECRET`,
|
|
114
|
+
`CLERK_SECRET_KEY`, `MCP_OAUTH_SIGNING_JWK`) answers `503` and is named in the
|
|
115
|
+
log (names, never values). There is no auto-approve path.
|
|
116
|
+
|
|
117
|
+
**Stores**: the authorization-code store and the client registry are interfaces
|
|
118
|
+
with in-memory adapters for tests and single-process development. Use durable
|
|
119
|
+
adapters in production — contract in [`docs/oauth-stores.md`](docs/oauth-stores.md).
|
|
120
|
+
|
|
121
|
+
**Not included**: refresh tokens (access tokens last one hour), per-organisation
|
|
122
|
+
API keys for non-ChatGPT clients (this code never had that path; it is a gap, not
|
|
123
|
+
a regression), wildcard redirect URIs, and a token revocation list.
|
|
124
|
+
|
|
125
|
+
### Multi-tenancy
|
|
126
|
+
|
|
127
|
+
`list_items` filters rows by their stored `orgId` against the caller's verified
|
|
128
|
+
organisation (`isRowInTenant`); a token with no organisation gets an explicit
|
|
129
|
+
tool refusal, never an unscoped view. Resolve the caller's tenant only from the
|
|
130
|
+
verified token, never from a tool argument.
|
|
131
|
+
|
|
132
|
+
## Server-side i18n (FR/EN)
|
|
133
|
+
|
|
134
|
+
`src/i18n.ts` localizes MCP error/validation messages and the picker page: the
|
|
135
|
+
client sends `X-MCP-Locale: fr|en` (default `en`); the browser picker follows
|
|
136
|
+
`Accept-Language`. Resource files (`src/locales/{en,fr}.json`) keep the
|
|
137
|
+
`<Namespace>.<category>.<key>` convention of `@vantageos/mosaic-i18n`, FR and EN
|
|
138
|
+
in lockstep.
|
|
91
139
|
|
|
92
140
|
## Testing
|
|
93
141
|
|
|
@@ -95,91 +143,80 @@ boilerplate's own `Mcp` namespace.
|
|
|
95
143
|
npm test
|
|
96
144
|
```
|
|
97
145
|
|
|
98
|
-
Runs both transports
|
|
99
|
-
|
|
100
|
-
`dist/stdio.js`
|
|
146
|
+
Runs both transports through the real SDK code paths: the HTTP suite drives the
|
|
147
|
+
Hono app in-process with the SDK v2 `Client`; the stdio suite spawns the built
|
|
148
|
+
`dist/stdio.js`. `npm run evals` runs the declarative case-set against stdio.
|
|
101
149
|
|
|
102
150
|
## Adding a tool
|
|
103
151
|
|
|
104
|
-
Register it once in `src/mcpServerFactory.ts`
|
|
105
|
-
|
|
152
|
+
Register it once in `src/mcpServerFactory.ts` with `title`, the four
|
|
153
|
+
annotations, `inputSchema` and `outputSchema`, and return
|
|
154
|
+
`structuredResult(...)`. It is then served on both transports and both
|
|
155
|
+
protocol eras. `tests/toolRecordComplete.test.ts` keeps it honest.
|
|
106
156
|
|
|
107
157
|
## Packaging & distribution
|
|
108
158
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
159
|
+
Four manifests are **generated** from `scripts/plugin-manifest.source.json` and
|
|
160
|
+
`package.json` by `scripts/sync-plugin-version.mjs` (part of `npm run build`).
|
|
161
|
+
Never edit them by hand; `tests/pluginManifests.test.ts` fails on drift.
|
|
162
|
+
|
|
163
|
+
| File | For |
|
|
164
|
+
|---|---|
|
|
165
|
+
| `.claude-plugin/plugin.json` | Claude Code marketplace; non-standard fields (`description_fr`, `category`, `bilingual`) sit under `metadata` |
|
|
166
|
+
| `.mcp.json` | Claude Code: `type: http`, URL only, no secret |
|
|
167
|
+
| `plugin.json` + `mcp.json` | OpenAI Agent Plugins; `streamable-http`, auth under `extensions["com.openai"].auth` (OAuth, CIMD preferred, DCR fallback) |
|
|
168
|
+
|
|
169
|
+
The server URL is one placeholder in the source file. Set it for a real
|
|
170
|
+
deployment with `MCP_SERVER_URL=https://your-host/mcp node scripts/sync-plugin-version.mjs`
|
|
171
|
+
(both entries change), or leave Claude's `${MCP_BOILERPLATE_URL:-<placeholder>}`
|
|
172
|
+
to be overridden by the installer. The onboarding skill is
|
|
173
|
+
`skills/connect-mcp-server/SKILL.md`, declared for OpenAI as
|
|
174
|
+
`extensions["com.openai"].onboardingSkill`.
|
|
123
175
|
|
|
124
176
|
### Scaffold → build → publish
|
|
125
177
|
|
|
126
178
|
```bash
|
|
127
|
-
# 1. Scaffold: clone this repo (or the npm tarball) as your server's skeleton.
|
|
128
179
|
git clone https://github.com/elpiarthera/vantage-mcp-boilerplate.git my-mcp
|
|
129
|
-
cd my-mcp && npm install
|
|
130
|
-
|
|
131
|
-
#
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
#
|
|
135
|
-
rm -rf dist node_modules/.vite && npm run build && npm test && npm run evals
|
|
136
|
-
|
|
137
|
-
# 4. Inspect the tarball before publishing — confirm dist/ ships and nothing leaks:
|
|
138
|
-
npm pack --dry-run
|
|
139
|
-
|
|
140
|
-
# 5. Publish to the two channels:
|
|
141
|
-
npm publish # npm registry (prepublishOnly re-runs build+test+evals)
|
|
142
|
-
# VantageRegistry catalogue: register .claude-plugin/plugin.json via
|
|
143
|
-
# the fleet create-plugin / generate-marketplace skill (VR upsert).
|
|
180
|
+
cd my-mcp && npm install
|
|
181
|
+
# Rename package.json "name"/"bin" and the names in scripts/plugin-manifest.source.json,
|
|
182
|
+
# then add your tools in src/mcpServerFactory.ts.
|
|
183
|
+
rm -rf dist && npm run build && npm run build:ui && npm test && npm run evals
|
|
184
|
+
npm pack --dry-run # confirm dist/, manifests and skills/ ship
|
|
185
|
+
npm publish # prepublishOnly re-runs build + test + evals
|
|
144
186
|
```
|
|
145
187
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
(the runnable package) and the **VantageRegistry / Claude Code marketplace
|
|
149
|
-
catalogue** (discovery, via `plugin.json`).
|
|
188
|
+
Publication is proven at the registry's `dist-tag`, never by the exit code of
|
|
189
|
+
`npm publish`.
|
|
150
190
|
|
|
151
191
|
## MCP Apps UI layer
|
|
152
192
|
|
|
153
|
-
`render_items_ui` renders `EXAMPLE_ITEMS` as an interactive widget
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
The
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
**
|
|
179
|
-
|
|
180
|
-
`
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
0.2.0 MUST be published to npm BEFORE this boilerplate can be installed or
|
|
184
|
-
published from the registry.** This is a strict ordering, not optional: publish
|
|
185
|
-
the kit first, confirm 0.2.0 is live at the registry, then the boilerplate.
|
|
193
|
+
`render_items_ui` renders `EXAMPLE_ITEMS` as an interactive widget
|
|
194
|
+
(`io.modelcontextprotocol/ui`, spec 2026-01-26).
|
|
195
|
+
|
|
196
|
+
- The tool links its `ui://` resource through the nested `_meta.ui.resourceUri`
|
|
197
|
+
with explicit `visibility: ["model", "app"]`. The tool is registered with
|
|
198
|
+
`server.registerTool`, not ext-apps' `registerAppTool`, because that helper
|
|
199
|
+
also writes the deprecated flat `_meta["ui/resourceUri"]` key (mcp-app-standard A-6).
|
|
200
|
+
- The resource declares `_meta.ui`: `csp` with `connectDomains`,
|
|
201
|
+
`resourceDomains`, `frameDomains` and `baseUriDomains` (all empty — the widget
|
|
202
|
+
makes no network request, loads no third-party asset and embeds no frame),
|
|
203
|
+
`prefersBorder`, and `domain` only when `MCP_UI_DOMAIN` is set (the format is
|
|
204
|
+
host-dependent). Its mime type is `text/html;profile=mcp-app`.
|
|
205
|
+
- **ChatGPT keys are additive**: `openai/outputTemplate` (same URI) and the
|
|
206
|
+
`openai/toolInvocation/invoking|invoked` strings. Nothing reads them; the
|
|
207
|
+
server works with every `openai/*` key absent. `openai/ui` entrypoints are
|
|
208
|
+
not emitted — they are optional; add them to the tool `_meta` in
|
|
209
|
+
`src/ui/uiResource.ts` if your ChatGPT app needs them.
|
|
210
|
+
- A markdown fallback is always in the call result, for clients that never
|
|
211
|
+
negotiate the extension.
|
|
212
|
+
|
|
213
|
+
The widget (`src/ui/`) reuses a vendored `MosaicCard` family and is bundled to
|
|
214
|
+
one self-contained HTML file (`vite-plugin-singlefile`,
|
|
215
|
+
`dist/ui-bundle/widget.html`) by `npm run build:ui`, on a path that never
|
|
216
|
+
collides with tsc's `dist/ui/`.
|
|
217
|
+
|
|
218
|
+
**Identity layer**: `@vantageos/cloud-identity` (^0.15.0) is the only identity
|
|
219
|
+
package. Its resource-server entry point (`verifyMcpAccessToken`,
|
|
220
|
+
`buildProtectedResourceMetadata`) verifies the Bearer token on `/mcp`; `jose` is
|
|
221
|
+
kept only to mint the authorization server's access tokens. An unreachable key set
|
|
222
|
+
answers 503, never 401.
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OAuth 2.1 resource-server wiring for this MCP server's HTTP transport.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* metadata URL) and
|
|
8
|
-
*
|
|
4
|
+
* Built on @vantageos/cloud-identity's resource-server entry point
|
|
5
|
+
* (`verifyMcpAccessToken`, `buildProtectedResourceMetadata`) — the ONLY identity layer of this
|
|
6
|
+
* boilerplate. This module never hand-rolls JWT verification. It only supplies configuration
|
|
7
|
+
* (issuer, audience, key set, resource metadata URL) and returns the verifier's result to the
|
|
8
|
+
* HTTP transport: accepted (with the verified session), refused 401 (with the RFC 6750
|
|
9
|
+
* challenge), or 503 when the key set is unreachable (an outage, never a bad credential).
|
|
9
10
|
*
|
|
10
11
|
* This is the "OAuth 2.1" tier of the two auth tiers this boilerplate
|
|
11
12
|
* supports (see config.ts `AuthMode`). The other tier — "public" — never
|
|
@@ -13,15 +14,24 @@
|
|
|
13
14
|
* stdio transport (stdio.ts) never had a gate to begin with (local process
|
|
14
15
|
* trust model, per mcp-standard.md).
|
|
15
16
|
*/
|
|
16
|
-
import { type
|
|
17
|
-
import type { JWTVerifyGetKey } from "jose";
|
|
17
|
+
import { type ClerkJwks, type VerifyMcpAccessTokenResult } from "@vantageos/cloud-identity";
|
|
18
18
|
export interface AuthConfig {
|
|
19
19
|
realm: string;
|
|
20
20
|
resourceMetadataUrl: string;
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The protected-resource identifier published in the PRM (RFC 9728). Defaults to the origin
|
|
23
|
+
* of `resourceMetadataUrl` (the pre-0.2 behaviour, kept for external-issuer deployments);
|
|
24
|
+
* the own-authorization-server tier sets it to `<base>/mcp`.
|
|
25
|
+
*/
|
|
26
|
+
resource?: string;
|
|
27
|
+
/**
|
|
28
|
+
* The signing keys: a `{ keys }` JWK set, or a function returning one. The consumer owns
|
|
29
|
+
* fetching and caching (config.ts `createCachedJwksFetcher` for an external issuer).
|
|
30
|
+
*/
|
|
31
|
+
jwks: ClerkJwks | (() => Promise<ClerkJwks>);
|
|
22
32
|
issuer: string;
|
|
23
33
|
audience: string;
|
|
24
34
|
authorizationServers: string[];
|
|
25
35
|
}
|
|
26
|
-
export declare function authenticateRequest(authHeader: string | undefined, config: AuthConfig): Promise<
|
|
27
|
-
export declare function protectedResourceMetadataDocument(config: AuthConfig): import("@vantageos/
|
|
36
|
+
export declare function authenticateRequest(authHeader: string | undefined, config: AuthConfig): Promise<VerifyMcpAccessTokenResult>;
|
|
37
|
+
export declare function protectedResourceMetadataDocument(config: AuthConfig): import("@vantageos/cloud-identity").ProtectedResourceMetadata;
|
package/dist/auth.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OAuth 2.1 resource-server wiring for this MCP server's HTTP transport.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* metadata URL) and
|
|
8
|
-
*
|
|
4
|
+
* Built on @vantageos/cloud-identity's resource-server entry point
|
|
5
|
+
* (`verifyMcpAccessToken`, `buildProtectedResourceMetadata`) — the ONLY identity layer of this
|
|
6
|
+
* boilerplate. This module never hand-rolls JWT verification. It only supplies configuration
|
|
7
|
+
* (issuer, audience, key set, resource metadata URL) and returns the verifier's result to the
|
|
8
|
+
* HTTP transport: accepted (with the verified session), refused 401 (with the RFC 6750
|
|
9
|
+
* challenge), or 503 when the key set is unreachable (an outage, never a bad credential).
|
|
9
10
|
*
|
|
10
11
|
* This is the "OAuth 2.1" tier of the two auth tiers this boilerplate
|
|
11
12
|
* supports (see config.ts `AuthMode`). The other tier — "public" — never
|
|
@@ -13,9 +14,9 @@
|
|
|
13
14
|
* stdio transport (stdio.ts) never had a gate to begin with (local process
|
|
14
15
|
* trust model, per mcp-standard.md).
|
|
15
16
|
*/
|
|
16
|
-
import { buildProtectedResourceMetadata,
|
|
17
|
+
import { buildProtectedResourceMetadata, verifyMcpAccessToken, } from "@vantageos/cloud-identity";
|
|
17
18
|
export async function authenticateRequest(authHeader, config) {
|
|
18
|
-
return
|
|
19
|
+
return verifyMcpAccessToken(authHeader, {
|
|
19
20
|
realm: config.realm,
|
|
20
21
|
resourceMetadataUrl: config.resourceMetadataUrl,
|
|
21
22
|
jwks: config.jwks,
|
|
@@ -25,7 +26,7 @@ export async function authenticateRequest(authHeader, config) {
|
|
|
25
26
|
}
|
|
26
27
|
export function protectedResourceMetadataDocument(config) {
|
|
27
28
|
return buildProtectedResourceMetadata({
|
|
28
|
-
resource: config.resourceMetadataUrl.replace(/\/\.well-known\/.*/, ""),
|
|
29
|
+
resource: config.resource ?? config.resourceMetadataUrl.replace(/\/\.well-known\/.*/, ""),
|
|
29
30
|
authorizationServers: config.authorizationServers,
|
|
30
31
|
});
|
|
31
32
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -13,9 +13,16 @@
|
|
|
13
13
|
* URL, issuer, audience) comes from the environment, never a literal in
|
|
14
14
|
* code.
|
|
15
15
|
*/
|
|
16
|
-
import
|
|
16
|
+
import type { ClerkJwks } from "@vantageos/cloud-identity";
|
|
17
17
|
import type { AuthConfig } from "./auth.js";
|
|
18
18
|
export type AuthMode = "oauth" | "public";
|
|
19
19
|
export declare function loadAuthMode(env?: NodeJS.ProcessEnv): AuthMode;
|
|
20
|
-
export declare function loadAuthConfig(env?: NodeJS.ProcessEnv, jwksOverride?:
|
|
21
|
-
|
|
20
|
+
export declare function loadAuthConfig(env?: NodeJS.ProcessEnv, jwksOverride?: AuthConfig["jwks"]): AuthConfig;
|
|
21
|
+
/**
|
|
22
|
+
* Fetch-and-cache for an external issuer's JWKS. cloud-identity's resource server takes the key
|
|
23
|
+
* set (or a function returning it) and owns no network I/O, so this boilerplate owns it: one
|
|
24
|
+
* GET of the JWKS document, cached for ten minutes. A failed fetch throws — the verifier turns
|
|
25
|
+
* that into a 503 (`jwks-unavailable`), never a 401 — unless a previously fetched set is still
|
|
26
|
+
* held, which is served rather than failing every request during a brief issuer blip.
|
|
27
|
+
*/
|
|
28
|
+
export declare function createCachedJwksFetcher(url: string, fetchImpl?: typeof fetch, nowMs?: () => number): () => Promise<ClerkJwks>;
|
package/dist/config.js
CHANGED
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
* URL, issuer, audience) comes from the environment, never a literal in
|
|
14
14
|
* code.
|
|
15
15
|
*/
|
|
16
|
-
import { createLocalJWKSet, createRemoteJWKSet } from "jose";
|
|
17
16
|
export function loadAuthMode(env = process.env) {
|
|
18
17
|
const raw = (env.AUTH_MODE ?? "oauth").toLowerCase();
|
|
19
18
|
if (raw !== "oauth" && raw !== "public") {
|
|
@@ -30,7 +29,7 @@ export function loadAuthConfig(env = process.env, jwksOverride) {
|
|
|
30
29
|
"oauth mode (OAuth 2.1 resource-server requirement — mcp-standard.md Critical Rule #14). " +
|
|
31
30
|
"Set AUTH_MODE=public instead to run the no-auth local/self-host tier.");
|
|
32
31
|
}
|
|
33
|
-
const jwks = jwksOverride ??
|
|
32
|
+
const jwks = jwksOverride ?? createCachedJwksFetcher(`${issuer}/.well-known/jwks.json`);
|
|
34
33
|
return {
|
|
35
34
|
realm: "mcp",
|
|
36
35
|
resourceMetadataUrl: `${publicBaseUrl}/.well-known/oauth-protected-resource`,
|
|
@@ -40,4 +39,33 @@ export function loadAuthConfig(env = process.env, jwksOverride) {
|
|
|
40
39
|
authorizationServers: [issuer],
|
|
41
40
|
};
|
|
42
41
|
}
|
|
43
|
-
|
|
42
|
+
const JWKS_TTL_MS = 10 * 60 * 1000;
|
|
43
|
+
/**
|
|
44
|
+
* Fetch-and-cache for an external issuer's JWKS. cloud-identity's resource server takes the key
|
|
45
|
+
* set (or a function returning it) and owns no network I/O, so this boilerplate owns it: one
|
|
46
|
+
* GET of the JWKS document, cached for ten minutes. A failed fetch throws — the verifier turns
|
|
47
|
+
* that into a 503 (`jwks-unavailable`), never a 401 — unless a previously fetched set is still
|
|
48
|
+
* held, which is served rather than failing every request during a brief issuer blip.
|
|
49
|
+
*/
|
|
50
|
+
export function createCachedJwksFetcher(url, fetchImpl = fetch, nowMs = Date.now) {
|
|
51
|
+
let cached;
|
|
52
|
+
return async () => {
|
|
53
|
+
if (cached && nowMs() - cached.fetchedAt < JWKS_TTL_MS)
|
|
54
|
+
return cached.jwks;
|
|
55
|
+
try {
|
|
56
|
+
const res = await fetchImpl(url);
|
|
57
|
+
if (!res.ok)
|
|
58
|
+
throw new Error(`JWKS fetch ${url} answered ${res.status}`);
|
|
59
|
+
const body = (await res.json());
|
|
60
|
+
if (!body || !Array.isArray(body.keys))
|
|
61
|
+
throw new Error(`JWKS document at ${url} has no keys`);
|
|
62
|
+
cached = { jwks: body, fetchedAt: nowMs() };
|
|
63
|
+
return body;
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
if (cached)
|
|
67
|
+
return cached.jwks;
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
package/dist/exampleItems.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Seed data for the `list_items` multi-tenant example tool.
|
|
3
|
-
* `
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Seed data for the `list_items` multi-tenant example tool. Each row is stamped with the
|
|
3
|
+
* stored `orgId` of the organisation that owns it — the row is selected by comparing that
|
|
4
|
+
* stored id with the caller's verified organisation (tenancy.ts `isRowInTenant`), never by
|
|
5
|
+
* a creator name. `createdBy` / `namespace` stay as display fields only.
|
|
6
6
|
*/
|
|
7
7
|
export interface ExampleItem {
|
|
8
8
|
id: string;
|
|
9
9
|
name: string;
|
|
10
|
+
orgId: string;
|
|
10
11
|
createdBy: string;
|
|
11
12
|
namespace: string;
|
|
12
13
|
}
|
package/dist/exampleItems.js
CHANGED
|
@@ -2,18 +2,21 @@ export const EXAMPLE_ITEMS = [
|
|
|
2
2
|
{
|
|
3
3
|
id: "item-1",
|
|
4
4
|
name: "Tenant A widget",
|
|
5
|
+
orgId: "org_a",
|
|
5
6
|
createdBy: "org_a",
|
|
6
7
|
namespace: "org_a",
|
|
7
8
|
},
|
|
8
9
|
{
|
|
9
10
|
id: "item-2",
|
|
10
11
|
name: "Tenant A gadget",
|
|
12
|
+
orgId: "org_a",
|
|
11
13
|
createdBy: "org_a",
|
|
12
14
|
namespace: "org_a",
|
|
13
15
|
},
|
|
14
16
|
{
|
|
15
17
|
id: "item-3",
|
|
16
18
|
name: "Tenant B widget",
|
|
19
|
+
orgId: "org_b",
|
|
17
20
|
createdBy: "org_b",
|
|
18
21
|
namespace: "org_b",
|
|
19
22
|
},
|
package/dist/http.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { loadAuthConfig, loadAuthMode } from "./config.js";
|
|
2
2
|
import { buildHttpApp } from "./httpApp.js";
|
|
3
3
|
const authMode = loadAuthMode();
|
|
4
|
-
const
|
|
4
|
+
const uiDomain = process.env.MCP_UI_DOMAIN || undefined;
|
|
5
|
+
// OAuth tier: MCP_OAUTH_ISSUER + MCP_OAUTH_AUDIENCE select an EXTERNAL issuer (resource server
|
|
6
|
+
// only); otherwise this server is its own authorization server (ruling R1) and reads its
|
|
7
|
+
// settings from the environment per request — a missing value answers 503, see
|
|
8
|
+
// src/oauth/settings.ts.
|
|
9
|
+
const externalIssuer = Boolean(process.env.MCP_OAUTH_ISSUER && process.env.MCP_OAUTH_AUDIENCE);
|
|
10
|
+
const app = buildHttpApp(authMode === "oauth"
|
|
11
|
+
? externalIssuer
|
|
12
|
+
? { authMode, auth: loadAuthConfig(), uiDomain }
|
|
13
|
+
: { authMode, authorizationServer: {}, uiDomain }
|
|
14
|
+
: { authMode, uiDomain });
|
|
5
15
|
const port = Number(process.env.PORT ?? 8787);
|
|
6
16
|
// Hono's fetch handler adapted onto Node's http server (no extra framework
|
|
7
17
|
// dependency beyond hono + node:http).
|