@tinycloud/mcp 0.8.3-beta.6 → 0.8.3-beta.8
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/README.md +25 -0
- package/dist/cli.cjs +3 -3
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +3 -3
- package/dist/cli.js.map +1 -1
- package/dist/http-cli.cjs +184 -15
- package/dist/http-cli.cjs.map +1 -1
- package/dist/http-cli.js +178 -9
- package/dist/http-cli.js.map +1 -1
- package/dist/http.cjs +172 -14
- package/dist/http.cjs.map +1 -1
- package/dist/http.d.cts +22 -0
- package/dist/http.d.ts +22 -0
- package/dist/http.js +166 -8
- package/dist/http.js.map +1 -1
- package/dist/index.cjs +172 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +166 -8
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +3 -3
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +3 -3
- package/dist/server.js.map +1 -1
- package/package.json +3 -3
package/dist/http-cli.js
CHANGED
|
@@ -33,7 +33,9 @@ function createOpenKeyTokenVerifier(config) {
|
|
|
33
33
|
issuer: config.issuer,
|
|
34
34
|
audience: config.audience
|
|
35
35
|
});
|
|
36
|
-
|
|
36
|
+
const info = authInfo(token, payload, config);
|
|
37
|
+
if (config.tokenStatus !== void 0) await requireUsableStatus(config.tokenStatus, token);
|
|
38
|
+
return info;
|
|
37
39
|
} catch (error) {
|
|
38
40
|
if (error instanceof errors.JOSEError) {
|
|
39
41
|
throw new OAuthError(OAuthErrorCode.InvalidToken, "The OpenKey access token is invalid.");
|
|
@@ -43,6 +45,16 @@ function createOpenKeyTokenVerifier(config) {
|
|
|
43
45
|
}
|
|
44
46
|
};
|
|
45
47
|
}
|
|
48
|
+
async function requireUsableStatus(checker, token) {
|
|
49
|
+
const status = await checker.tokenStatus(token);
|
|
50
|
+
if (status === "active") return;
|
|
51
|
+
if (status === "inactive") {
|
|
52
|
+
throw new OAuthError(OAuthErrorCode.InvalidToken, "The OpenKey access token is no longer active.");
|
|
53
|
+
}
|
|
54
|
+
if (!checker.toleratesUnknown(token)) {
|
|
55
|
+
throw new OAuthError(OAuthErrorCode.ServerError, "The OpenKey access token status is unavailable.");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
46
58
|
async function loadOAuthMetadata(metadataUrl) {
|
|
47
59
|
const response = await fetch(metadataUrl, {
|
|
48
60
|
headers: { accept: "application/json" },
|
|
@@ -108,6 +120,150 @@ function firstString(...values) {
|
|
|
108
120
|
return values.find((value) => typeof value === "string" && value.length > 0);
|
|
109
121
|
}
|
|
110
122
|
|
|
123
|
+
// src/token-status.ts
|
|
124
|
+
import { createHash } from "crypto";
|
|
125
|
+
import { decodeJwt } from "jose";
|
|
126
|
+
var ACTIVE_CACHE_MS = 3e4;
|
|
127
|
+
var DEFAULT_TOKEN_LIFETIME_SECONDS = 300;
|
|
128
|
+
var DEFAULT_TIMEOUT_MS = 3e3;
|
|
129
|
+
var MAX_CACHED_TOKENS = 1e4;
|
|
130
|
+
function createTokenStatusChecker(config) {
|
|
131
|
+
const doFetch = config.fetch ?? fetch;
|
|
132
|
+
const clock = config.now ?? Date.now;
|
|
133
|
+
const log = config.log ?? ((message) => void process.stderr.write(`[tinycloud-mcp-http] ${message}
|
|
134
|
+
`));
|
|
135
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
136
|
+
const inactive = /* @__PURE__ */ new Map();
|
|
137
|
+
const active = /* @__PURE__ */ new Map();
|
|
138
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
139
|
+
const confirmedActive = /* @__PURE__ */ new Map();
|
|
140
|
+
let credentialsRefused = false;
|
|
141
|
+
async function introspect(token) {
|
|
142
|
+
const headers = {
|
|
143
|
+
accept: "application/json",
|
|
144
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
145
|
+
};
|
|
146
|
+
const body = new URLSearchParams({ token, token_type_hint: "access_token" });
|
|
147
|
+
if (config.authMethod === "basic") {
|
|
148
|
+
const encode = (value) => encodeURIComponent(value).replace(/%20/g, "+");
|
|
149
|
+
const credentials = `${encode(config.clientId)}:${encode(config.clientSecret)}`;
|
|
150
|
+
headers.authorization = `Basic ${Buffer.from(credentials).toString("base64")}`;
|
|
151
|
+
} else {
|
|
152
|
+
body.set("client_id", config.clientId);
|
|
153
|
+
body.set("client_secret", config.clientSecret);
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const response = await doFetch(config.endpoint, {
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers,
|
|
159
|
+
body,
|
|
160
|
+
redirect: "error",
|
|
161
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
162
|
+
});
|
|
163
|
+
if (response.status === 401) return "credentials";
|
|
164
|
+
if (response.status !== 200) return "unknown";
|
|
165
|
+
const result = await response.json();
|
|
166
|
+
if (typeof result !== "object" || result === null || !("active" in result)) return "unknown";
|
|
167
|
+
if (result.active === false) return "inactive";
|
|
168
|
+
if (result.active !== true) return "unknown";
|
|
169
|
+
return answerMatchesToken(Object.fromEntries(Object.entries(result)), token, config.resourceUrl) ? "active" : "mismatch";
|
|
170
|
+
} catch {
|
|
171
|
+
return "unknown";
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function query(key, token) {
|
|
175
|
+
const outcome = await introspect(token);
|
|
176
|
+
if (outcome === "credentials") {
|
|
177
|
+
if (!credentialsRefused) {
|
|
178
|
+
log("OpenKey token introspection refused the MCP client credentials (401); token checks fail closed until it accepts them.");
|
|
179
|
+
}
|
|
180
|
+
credentialsRefused = true;
|
|
181
|
+
return "unknown";
|
|
182
|
+
}
|
|
183
|
+
if (outcome === "unknown") return outcome;
|
|
184
|
+
const now = clock();
|
|
185
|
+
prune(now);
|
|
186
|
+
if (outcome === "mismatch") {
|
|
187
|
+
inactive.set(key, tokenExpiryMs(token, now));
|
|
188
|
+
confirmedActive.delete(key);
|
|
189
|
+
return "inactive";
|
|
190
|
+
}
|
|
191
|
+
credentialsRefused = false;
|
|
192
|
+
if (outcome === "inactive") {
|
|
193
|
+
inactive.set(key, tokenExpiryMs(token, now));
|
|
194
|
+
confirmedActive.delete(key);
|
|
195
|
+
} else {
|
|
196
|
+
active.set(key, now + ACTIVE_CACHE_MS);
|
|
197
|
+
confirmedActive.delete(key);
|
|
198
|
+
confirmedActive.set(key, { at: now, expiresAt: tokenExpiryMs(token, now) });
|
|
199
|
+
}
|
|
200
|
+
return outcome;
|
|
201
|
+
}
|
|
202
|
+
function prune(now) {
|
|
203
|
+
for (const [key, until] of active) if (until <= now) active.delete(key);
|
|
204
|
+
for (const [key, until] of inactive) if (until <= now) inactive.delete(key);
|
|
205
|
+
for (const [key, entry] of confirmedActive) if (entry.expiresAt <= now) confirmedActive.delete(key);
|
|
206
|
+
for (const cache of [active, inactive, confirmedActive]) {
|
|
207
|
+
while (cache.size >= MAX_CACHED_TOKENS) {
|
|
208
|
+
const oldest = cache.keys().next().value;
|
|
209
|
+
if (oldest === void 0) break;
|
|
210
|
+
cache.delete(oldest);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
async tokenStatus(token) {
|
|
216
|
+
const key = createHash("sha256").update(token).digest("hex");
|
|
217
|
+
const now = clock();
|
|
218
|
+
const inactiveUntil = inactive.get(key);
|
|
219
|
+
if (inactiveUntil !== void 0 && inactiveUntil > now) return "inactive";
|
|
220
|
+
const activeUntil = active.get(key);
|
|
221
|
+
if (activeUntil !== void 0 && activeUntil > now) return "active";
|
|
222
|
+
const pending = inFlight.get(key);
|
|
223
|
+
if (pending !== void 0) return pending;
|
|
224
|
+
const asking = query(key, token).finally(() => inFlight.delete(key));
|
|
225
|
+
inFlight.set(key, asking);
|
|
226
|
+
return asking;
|
|
227
|
+
},
|
|
228
|
+
toleratesUnknown(token) {
|
|
229
|
+
if (credentialsRefused) return false;
|
|
230
|
+
const confirmed = confirmedActive.get(createHash("sha256").update(token).digest("hex"));
|
|
231
|
+
if (confirmed === void 0) return false;
|
|
232
|
+
const now = clock();
|
|
233
|
+
return confirmed.expiresAt > now && now - confirmed.at < tokenLifetimeSeconds(token) * 1e3;
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function answerMatchesToken(answer, token, resourceUrl) {
|
|
238
|
+
const jwt = claims(token);
|
|
239
|
+
if (typeof jwt.sub === "string" && answer.sub !== jwt.sub) return false;
|
|
240
|
+
const jwtClient = [jwt.azp, jwt.client_id].find((value) => typeof value === "string" && value.length > 0);
|
|
241
|
+
if (jwtClient !== void 0 && answer.client_id !== jwtClient) return false;
|
|
242
|
+
if (jwt.aud === void 0) return true;
|
|
243
|
+
const answered = audienceList(answer.aud);
|
|
244
|
+
const signed = audienceList(jwt.aud);
|
|
245
|
+
return answered.length > 0 && answered.includes(resourceUrl) && answered.every((entry) => signed.includes(entry));
|
|
246
|
+
}
|
|
247
|
+
function audienceList(value) {
|
|
248
|
+
const list = typeof value === "string" ? [value] : value;
|
|
249
|
+
return Array.isArray(list) && list.every((entry) => typeof entry === "string") ? list : [];
|
|
250
|
+
}
|
|
251
|
+
function tokenExpiryMs(token, now) {
|
|
252
|
+
const exp = claims(token).exp;
|
|
253
|
+
return typeof exp === "number" ? exp * 1e3 : now + DEFAULT_TOKEN_LIFETIME_SECONDS * 1e3;
|
|
254
|
+
}
|
|
255
|
+
function tokenLifetimeSeconds(token) {
|
|
256
|
+
const { exp, iat } = claims(token);
|
|
257
|
+
return typeof exp === "number" && typeof iat === "number" && exp > iat ? exp - iat : DEFAULT_TOKEN_LIFETIME_SECONDS;
|
|
258
|
+
}
|
|
259
|
+
function claims(token) {
|
|
260
|
+
try {
|
|
261
|
+
return decodeJwt(token);
|
|
262
|
+
} catch {
|
|
263
|
+
return {};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
111
267
|
// src/logging.ts
|
|
112
268
|
function logToolCall(tool, code, startedAt, replayedGrants) {
|
|
113
269
|
process.stderr.write(`${JSON.stringify({
|
|
@@ -1394,7 +1550,7 @@ function outcomeCode(result) {
|
|
|
1394
1550
|
// package.json
|
|
1395
1551
|
var package_default = {
|
|
1396
1552
|
name: "@tinycloud/mcp",
|
|
1397
|
-
version: "0.8.3-beta.
|
|
1553
|
+
version: "0.8.3-beta.8",
|
|
1398
1554
|
description: "TinyCloud delegated operations MCP server",
|
|
1399
1555
|
keywords: [
|
|
1400
1556
|
"tinycloud",
|
|
@@ -1465,9 +1621,9 @@ var package_default = {
|
|
|
1465
1621
|
dependencies: {
|
|
1466
1622
|
"@modelcontextprotocol/node": "2.0.0-beta.5",
|
|
1467
1623
|
"@modelcontextprotocol/server": "2.0.0-beta.5",
|
|
1468
|
-
"@tinycloud/node-sdk": "3.8.0-beta.
|
|
1624
|
+
"@tinycloud/node-sdk": "3.8.0-beta.6",
|
|
1469
1625
|
"@tinycloud/node-sdk-wasm": "1.7.8",
|
|
1470
|
-
"@tinycloud/operations": "0.7.7-beta.
|
|
1626
|
+
"@tinycloud/operations": "0.7.7-beta.7",
|
|
1471
1627
|
jose: "6.2.4"
|
|
1472
1628
|
},
|
|
1473
1629
|
devDependencies: {
|
|
@@ -1494,12 +1650,13 @@ var REQUIRED_SCOPE = "tinycloud:mcp";
|
|
|
1494
1650
|
function createHostedMcpApp(config) {
|
|
1495
1651
|
const resourceUrl = canonicalResourceUrl(config.publicUrl);
|
|
1496
1652
|
const pluginUrl = new URL("./chatgpt/mcp", resourceUrl);
|
|
1497
|
-
const
|
|
1653
|
+
const statusFor = (resource) => config.introspection === void 0 ? void 0 : createTokenStatusChecker({ ...config.introspection, resourceUrl: resource.toString() });
|
|
1654
|
+
const hosted = createHostedSurface(config, resourceUrl, "hosted", statusFor(resourceUrl));
|
|
1498
1655
|
const plugin = createHostedSurface({
|
|
1499
1656
|
...config,
|
|
1500
1657
|
stateDir: join3(config.stateDir, "chatgpt"),
|
|
1501
1658
|
approvalPath: new URL("./connect", pluginUrl).pathname
|
|
1502
|
-
}, pluginUrl, "public");
|
|
1659
|
+
}, pluginUrl, "public", statusFor(pluginUrl));
|
|
1503
1660
|
const pluginPaths = {
|
|
1504
1661
|
[pluginUrl.pathname]: true,
|
|
1505
1662
|
[new URL("./connect", pluginUrl).pathname]: true,
|
|
@@ -1513,7 +1670,7 @@ function createHostedMcpApp(config) {
|
|
|
1513
1670
|
}
|
|
1514
1671
|
};
|
|
1515
1672
|
}
|
|
1516
|
-
function createHostedSurface(config, resourceUrl, surface) {
|
|
1673
|
+
function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
|
|
1517
1674
|
const connectPath = config.approvalPath ?? "/connect";
|
|
1518
1675
|
const callbackPath = `${connectPath}/callback`;
|
|
1519
1676
|
const issuer = String(config.oauthMetadata.issuer);
|
|
@@ -1522,7 +1679,8 @@ function createHostedSurface(config, resourceUrl, surface) {
|
|
|
1522
1679
|
issuer,
|
|
1523
1680
|
audience: resourceUrl.toString(),
|
|
1524
1681
|
jwksUri,
|
|
1525
|
-
requiredScope: REQUIRED_SCOPE
|
|
1682
|
+
requiredScope: REQUIRED_SCOPE,
|
|
1683
|
+
...tokenStatus === void 0 ? {} : { tokenStatus }
|
|
1526
1684
|
});
|
|
1527
1685
|
const resourceMetadataUrl = getOAuthProtectedResourceMetadataUrl(resourceUrl);
|
|
1528
1686
|
const authenticate = requireBearerAuth({
|
|
@@ -1774,7 +1932,13 @@ async function main() {
|
|
|
1774
1932
|
approvalTtlSeconds: positiveInteger(process.env.TC_MCP_APPROVAL_TTL_SECONDS, 300),
|
|
1775
1933
|
delegationExpiry: process.env.TC_MCP_DELEGATION_EXPIRY ?? "1h",
|
|
1776
1934
|
allowedOrigins: commaList(process.env.TC_MCP_ALLOWED_ORIGINS),
|
|
1777
|
-
oauthMetadata
|
|
1935
|
+
oauthMetadata,
|
|
1936
|
+
introspection: {
|
|
1937
|
+
endpoint: requiredUrl("TC_MCP_INTROSPECTION_URL").toString(),
|
|
1938
|
+
clientId: requiredString("TC_MCP_INTROSPECTION_CLIENT_ID"),
|
|
1939
|
+
clientSecret: requiredString("TC_MCP_INTROSPECTION_CLIENT_SECRET"),
|
|
1940
|
+
authMethod: introspectionAuthMethod(process.env.TC_MCP_INTROSPECTION_AUTH_METHOD)
|
|
1941
|
+
}
|
|
1778
1942
|
});
|
|
1779
1943
|
const nodeHandler = toNodeHandler(app);
|
|
1780
1944
|
const server = createServer(nodeHandler);
|
|
@@ -1807,6 +1971,11 @@ function requiredAbsolutePath(name) {
|
|
|
1807
1971
|
if (!isAbsolute(value)) throw new Error(`${name} must be an absolute path.`);
|
|
1808
1972
|
return resolve(value);
|
|
1809
1973
|
}
|
|
1974
|
+
function introspectionAuthMethod(value) {
|
|
1975
|
+
if (value === void 0 || value === "basic") return "basic";
|
|
1976
|
+
if (value === "post") return "post";
|
|
1977
|
+
throw new Error("TC_MCP_INTROSPECTION_AUTH_METHOD must be basic or post.");
|
|
1978
|
+
}
|
|
1810
1979
|
function positiveInteger(value, fallback) {
|
|
1811
1980
|
if (value === void 0) return fallback;
|
|
1812
1981
|
const parsed = Number(value);
|