@cloudgrid-io/mcp 0.21.2 → 0.21.4
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/package.json +1 -1
- package/src/web-identity.js +22 -0
- package/src/web.js +35 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudgrid-io/mcp",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.4",
|
|
4
4
|
"description": "MCP access to CloudGrid — the live runtime environment where the user's apps run WITH the infrastructure they need out of the box: managed MongoDB, Redis (cache/queues/pub-sub), a pgvector store, persistent disk, and built-in AI with no API keys, in any language or stack, no Docker or Kubernetes to manage. Brainstorm, build, plug, and improve apps, games, dashboards, and tools; instant single-page publishes and full runtime-app deploys with a live URL — for developers and non-technical users. Three editions: local stdio (full toolset), hosted web (light, CLI-free), and a public docs-search server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/web-identity.js
CHANGED
|
@@ -5,6 +5,19 @@ function isTokenExpired(jwt, now) {
|
|
|
5
5
|
return decodeJwt(jwt).exp * 1000 <= now();
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
// Is this token a credential the server can actually authenticate with right
|
|
9
|
+
// now? A present, decodable token whose numeric `exp` is still in the future.
|
|
10
|
+
// Anything else — absent, undecodable, or lacking a real expiry — is unusable.
|
|
11
|
+
// Unlike isTokenExpired (which treats an undecodable token as "not expired"
|
|
12
|
+
// because NaN <= now is false), this answers the transport guard's question,
|
|
13
|
+
// where a token we cannot vouch for must not pass.
|
|
14
|
+
function isCredentialUsable(jwt, now) {
|
|
15
|
+
if (!jwt) return false;
|
|
16
|
+
const { exp } = decodeJwt(jwt);
|
|
17
|
+
if (typeof exp !== "number" || !Number.isFinite(exp)) return false;
|
|
18
|
+
return exp * 1000 > now();
|
|
19
|
+
}
|
|
20
|
+
|
|
8
21
|
export function createWebIdentity({
|
|
9
22
|
initialTransportToken = null,
|
|
10
23
|
now = () => Date.now(),
|
|
@@ -33,6 +46,15 @@ export function createWebIdentity({
|
|
|
33
46
|
return { identityChanged };
|
|
34
47
|
},
|
|
35
48
|
|
|
49
|
+
// Whether the session currently holds any credential the server can use:
|
|
50
|
+
// the effective token (explicit login shadowing transport per #279) is
|
|
51
|
+
// present, decodable, and unexpired. The transport guard asks this before
|
|
52
|
+
// accepting a request, so an expired/invalid Bearer with no valid explicit
|
|
53
|
+
// login is challenged rather than degrading to an in-band login link.
|
|
54
|
+
hasUsableCredential() {
|
|
55
|
+
return isCredentialUsable(effectiveToken(), now);
|
|
56
|
+
},
|
|
57
|
+
|
|
36
58
|
async getToken() {
|
|
37
59
|
const jwt = effectiveToken();
|
|
38
60
|
return isTokenExpired(jwt, now) ? null : jwt;
|
package/src/web.js
CHANGED
|
@@ -55,8 +55,9 @@ function bearerOf(req) {
|
|
|
55
55
|
|
|
56
56
|
// A web session: identity lives in memory for the session's lifetime only. The
|
|
57
57
|
// session id doubles as the stable, opaque end-user id for the trusted-server cap.
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
// The identity is built by the caller so the transport auth guard can consult it
|
|
59
|
+
// (hasUsableCredential) before the session is wired up.
|
|
60
|
+
function makeWebContext(sessionId, identity) {
|
|
60
61
|
return {
|
|
61
62
|
edition: "web",
|
|
62
63
|
state: { pendingLoginCode: null, lastAnonClaim: null, lastDrop: null, anonCookie: null },
|
|
@@ -89,6 +90,19 @@ mountOAuth(app, PUBLIC_BASE, { requireAuth: REQUIRE_AUTH });
|
|
|
89
90
|
const transports = Object.create(null);
|
|
90
91
|
const sessionContexts = Object.create(null);
|
|
91
92
|
|
|
93
|
+
// Per the MCP authorization spec, a resource server that receives no usable
|
|
94
|
+
// credential answers 401 with a Bearer WWW-Authenticate challenge — the signal
|
|
95
|
+
// Claude web needs to re-run OAuth and render its native login UI. Reused for
|
|
96
|
+
// the missing, expired, and invalid transport-token cases.
|
|
97
|
+
function sendAuthChallenge(res) {
|
|
98
|
+
res.setHeader("WWW-Authenticate", bearerChallenge(PUBLIC_BASE));
|
|
99
|
+
res.status(401).json({
|
|
100
|
+
jsonrpc: "2.0",
|
|
101
|
+
error: { code: -32001, message: "Authorization required. Complete the OAuth connect." },
|
|
102
|
+
id: null,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
92
106
|
function captureRequestIdentity(sessionId, jwt) {
|
|
93
107
|
if (!jwt) return;
|
|
94
108
|
const ctx = sessionContexts[sessionId];
|
|
@@ -107,17 +121,22 @@ app.post("/mcp", async (req, res) => {
|
|
|
107
121
|
const jwt = bearerOf(req);
|
|
108
122
|
|
|
109
123
|
if (REQUIRE_AUTH && !jwt) {
|
|
110
|
-
res
|
|
111
|
-
res.status(401).json({
|
|
112
|
-
jsonrpc: "2.0",
|
|
113
|
-
error: { code: -32001, message: "Authorization required. Complete the OAuth connect." },
|
|
114
|
-
id: null,
|
|
115
|
-
});
|
|
124
|
+
sendAuthChallenge(res);
|
|
116
125
|
return;
|
|
117
126
|
}
|
|
118
127
|
|
|
119
128
|
if (transport) {
|
|
120
129
|
captureRequestIdentity(sessionId, jwt);
|
|
130
|
+
// An expired/invalid transport token with no valid explicit login means the
|
|
131
|
+
// session holds no usable credential — challenge so the client re-runs OAuth,
|
|
132
|
+
// rather than proceeding and degrading to an in-band copy-paste login link
|
|
133
|
+
// (#286). Consult the identity, not the raw Bearer: an explicit in-session
|
|
134
|
+
// login stays authoritative over an expired transport token (#279/#280).
|
|
135
|
+
const ctx = sessionContexts[sessionId];
|
|
136
|
+
if (REQUIRE_AUTH && ctx && !ctx.identity.hasUsableCredential()) {
|
|
137
|
+
sendAuthChallenge(res);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
121
140
|
await transport.handleRequest(req, res, req.body);
|
|
122
141
|
return;
|
|
123
142
|
}
|
|
@@ -135,6 +154,13 @@ app.post("/mcp", async (req, res) => {
|
|
|
135
154
|
// id up front so it is also the trusted-server end-user id. (Distinct name from
|
|
136
155
|
// the incoming `sessionId` header above.)
|
|
137
156
|
const newSessionId = randomUUID();
|
|
157
|
+
const identity = createWebIdentity({ initialTransportToken: jwt });
|
|
158
|
+
// A brand-new session has no explicit login yet, so the transport Bearer is the
|
|
159
|
+
// only possible credential — challenge if it is expired or undecodable (#286).
|
|
160
|
+
if (REQUIRE_AUTH && !identity.hasUsableCredential()) {
|
|
161
|
+
sendAuthChallenge(res);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
138
164
|
transport = new StreamableHTTPServerTransport({
|
|
139
165
|
sessionIdGenerator: () => newSessionId,
|
|
140
166
|
onsessioninitialized: (sid) => {
|
|
@@ -151,7 +177,7 @@ app.post("/mcp", async (req, res) => {
|
|
|
151
177
|
}
|
|
152
178
|
};
|
|
153
179
|
const server = new McpServer({ name: "cloudgrid-mcp-web", version }, { instructions: INSTRUCTIONS_WEB });
|
|
154
|
-
const webCtx = makeWebContext(newSessionId,
|
|
180
|
+
const webCtx = makeWebContext(newSessionId, identity);
|
|
155
181
|
sessionContexts[newSessionId] = webCtx;
|
|
156
182
|
// QA session log for this MCP session (dark by default). Keyed by the session
|
|
157
183
|
// id — the host connection boundary. No seed user_request here: current hosts
|