@tinycloud/mcp 0.8.3-beta.7 → 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 +1 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/http-cli.cjs +182 -13
- package/dist/http-cli.cjs.map +1 -1
- package/dist/http-cli.js +176 -7
- package/dist/http-cli.js.map +1 -1
- package/dist/http.cjs +170 -12
- 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 +164 -6
- package/dist/http.js.map +1 -1
- package/dist/index.cjs +170 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +164 -6
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +1 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/dist/http.cjs
CHANGED
|
@@ -41,7 +41,9 @@ function createOpenKeyTokenVerifier(config) {
|
|
|
41
41
|
issuer: config.issuer,
|
|
42
42
|
audience: config.audience
|
|
43
43
|
});
|
|
44
|
-
|
|
44
|
+
const info = authInfo(token, payload, config);
|
|
45
|
+
if (config.tokenStatus !== void 0) await requireUsableStatus(config.tokenStatus, token);
|
|
46
|
+
return info;
|
|
45
47
|
} catch (error) {
|
|
46
48
|
if (error instanceof import_jose.errors.JOSEError) {
|
|
47
49
|
throw new import_server.OAuthError(import_server.OAuthErrorCode.InvalidToken, "The OpenKey access token is invalid.");
|
|
@@ -51,6 +53,16 @@ function createOpenKeyTokenVerifier(config) {
|
|
|
51
53
|
}
|
|
52
54
|
};
|
|
53
55
|
}
|
|
56
|
+
async function requireUsableStatus(checker, token) {
|
|
57
|
+
const status = await checker.tokenStatus(token);
|
|
58
|
+
if (status === "active") return;
|
|
59
|
+
if (status === "inactive") {
|
|
60
|
+
throw new import_server.OAuthError(import_server.OAuthErrorCode.InvalidToken, "The OpenKey access token is no longer active.");
|
|
61
|
+
}
|
|
62
|
+
if (!checker.toleratesUnknown(token)) {
|
|
63
|
+
throw new import_server.OAuthError(import_server.OAuthErrorCode.ServerError, "The OpenKey access token status is unavailable.");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
54
66
|
function authInfo(token, payload, config) {
|
|
55
67
|
if (typeof payload.sub !== "string" || payload.sub.length === 0) {
|
|
56
68
|
throw new import_server.OAuthError(import_server.OAuthErrorCode.InvalidToken, "The OpenKey access token does not identify a subject.");
|
|
@@ -104,6 +116,150 @@ function firstString(...values) {
|
|
|
104
116
|
return values.find((value) => typeof value === "string" && value.length > 0);
|
|
105
117
|
}
|
|
106
118
|
|
|
119
|
+
// src/token-status.ts
|
|
120
|
+
var import_node_crypto = require("crypto");
|
|
121
|
+
var import_jose2 = require("jose");
|
|
122
|
+
var ACTIVE_CACHE_MS = 3e4;
|
|
123
|
+
var DEFAULT_TOKEN_LIFETIME_SECONDS = 300;
|
|
124
|
+
var DEFAULT_TIMEOUT_MS = 3e3;
|
|
125
|
+
var MAX_CACHED_TOKENS = 1e4;
|
|
126
|
+
function createTokenStatusChecker(config) {
|
|
127
|
+
const doFetch = config.fetch ?? fetch;
|
|
128
|
+
const clock = config.now ?? Date.now;
|
|
129
|
+
const log = config.log ?? ((message) => void process.stderr.write(`[tinycloud-mcp-http] ${message}
|
|
130
|
+
`));
|
|
131
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
132
|
+
const inactive = /* @__PURE__ */ new Map();
|
|
133
|
+
const active = /* @__PURE__ */ new Map();
|
|
134
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
135
|
+
const confirmedActive = /* @__PURE__ */ new Map();
|
|
136
|
+
let credentialsRefused = false;
|
|
137
|
+
async function introspect(token) {
|
|
138
|
+
const headers = {
|
|
139
|
+
accept: "application/json",
|
|
140
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
141
|
+
};
|
|
142
|
+
const body = new URLSearchParams({ token, token_type_hint: "access_token" });
|
|
143
|
+
if (config.authMethod === "basic") {
|
|
144
|
+
const encode = (value) => encodeURIComponent(value).replace(/%20/g, "+");
|
|
145
|
+
const credentials = `${encode(config.clientId)}:${encode(config.clientSecret)}`;
|
|
146
|
+
headers.authorization = `Basic ${Buffer.from(credentials).toString("base64")}`;
|
|
147
|
+
} else {
|
|
148
|
+
body.set("client_id", config.clientId);
|
|
149
|
+
body.set("client_secret", config.clientSecret);
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const response = await doFetch(config.endpoint, {
|
|
153
|
+
method: "POST",
|
|
154
|
+
headers,
|
|
155
|
+
body,
|
|
156
|
+
redirect: "error",
|
|
157
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
158
|
+
});
|
|
159
|
+
if (response.status === 401) return "credentials";
|
|
160
|
+
if (response.status !== 200) return "unknown";
|
|
161
|
+
const result = await response.json();
|
|
162
|
+
if (typeof result !== "object" || result === null || !("active" in result)) return "unknown";
|
|
163
|
+
if (result.active === false) return "inactive";
|
|
164
|
+
if (result.active !== true) return "unknown";
|
|
165
|
+
return answerMatchesToken(Object.fromEntries(Object.entries(result)), token, config.resourceUrl) ? "active" : "mismatch";
|
|
166
|
+
} catch {
|
|
167
|
+
return "unknown";
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function query(key, token) {
|
|
171
|
+
const outcome = await introspect(token);
|
|
172
|
+
if (outcome === "credentials") {
|
|
173
|
+
if (!credentialsRefused) {
|
|
174
|
+
log("OpenKey token introspection refused the MCP client credentials (401); token checks fail closed until it accepts them.");
|
|
175
|
+
}
|
|
176
|
+
credentialsRefused = true;
|
|
177
|
+
return "unknown";
|
|
178
|
+
}
|
|
179
|
+
if (outcome === "unknown") return outcome;
|
|
180
|
+
const now = clock();
|
|
181
|
+
prune(now);
|
|
182
|
+
if (outcome === "mismatch") {
|
|
183
|
+
inactive.set(key, tokenExpiryMs(token, now));
|
|
184
|
+
confirmedActive.delete(key);
|
|
185
|
+
return "inactive";
|
|
186
|
+
}
|
|
187
|
+
credentialsRefused = false;
|
|
188
|
+
if (outcome === "inactive") {
|
|
189
|
+
inactive.set(key, tokenExpiryMs(token, now));
|
|
190
|
+
confirmedActive.delete(key);
|
|
191
|
+
} else {
|
|
192
|
+
active.set(key, now + ACTIVE_CACHE_MS);
|
|
193
|
+
confirmedActive.delete(key);
|
|
194
|
+
confirmedActive.set(key, { at: now, expiresAt: tokenExpiryMs(token, now) });
|
|
195
|
+
}
|
|
196
|
+
return outcome;
|
|
197
|
+
}
|
|
198
|
+
function prune(now) {
|
|
199
|
+
for (const [key, until] of active) if (until <= now) active.delete(key);
|
|
200
|
+
for (const [key, until] of inactive) if (until <= now) inactive.delete(key);
|
|
201
|
+
for (const [key, entry] of confirmedActive) if (entry.expiresAt <= now) confirmedActive.delete(key);
|
|
202
|
+
for (const cache of [active, inactive, confirmedActive]) {
|
|
203
|
+
while (cache.size >= MAX_CACHED_TOKENS) {
|
|
204
|
+
const oldest = cache.keys().next().value;
|
|
205
|
+
if (oldest === void 0) break;
|
|
206
|
+
cache.delete(oldest);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
async tokenStatus(token) {
|
|
212
|
+
const key = (0, import_node_crypto.createHash)("sha256").update(token).digest("hex");
|
|
213
|
+
const now = clock();
|
|
214
|
+
const inactiveUntil = inactive.get(key);
|
|
215
|
+
if (inactiveUntil !== void 0 && inactiveUntil > now) return "inactive";
|
|
216
|
+
const activeUntil = active.get(key);
|
|
217
|
+
if (activeUntil !== void 0 && activeUntil > now) return "active";
|
|
218
|
+
const pending = inFlight.get(key);
|
|
219
|
+
if (pending !== void 0) return pending;
|
|
220
|
+
const asking = query(key, token).finally(() => inFlight.delete(key));
|
|
221
|
+
inFlight.set(key, asking);
|
|
222
|
+
return asking;
|
|
223
|
+
},
|
|
224
|
+
toleratesUnknown(token) {
|
|
225
|
+
if (credentialsRefused) return false;
|
|
226
|
+
const confirmed = confirmedActive.get((0, import_node_crypto.createHash)("sha256").update(token).digest("hex"));
|
|
227
|
+
if (confirmed === void 0) return false;
|
|
228
|
+
const now = clock();
|
|
229
|
+
return confirmed.expiresAt > now && now - confirmed.at < tokenLifetimeSeconds(token) * 1e3;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function answerMatchesToken(answer, token, resourceUrl) {
|
|
234
|
+
const jwt = claims(token);
|
|
235
|
+
if (typeof jwt.sub === "string" && answer.sub !== jwt.sub) return false;
|
|
236
|
+
const jwtClient = [jwt.azp, jwt.client_id].find((value) => typeof value === "string" && value.length > 0);
|
|
237
|
+
if (jwtClient !== void 0 && answer.client_id !== jwtClient) return false;
|
|
238
|
+
if (jwt.aud === void 0) return true;
|
|
239
|
+
const answered = audienceList(answer.aud);
|
|
240
|
+
const signed = audienceList(jwt.aud);
|
|
241
|
+
return answered.length > 0 && answered.includes(resourceUrl) && answered.every((entry) => signed.includes(entry));
|
|
242
|
+
}
|
|
243
|
+
function audienceList(value) {
|
|
244
|
+
const list = typeof value === "string" ? [value] : value;
|
|
245
|
+
return Array.isArray(list) && list.every((entry) => typeof entry === "string") ? list : [];
|
|
246
|
+
}
|
|
247
|
+
function tokenExpiryMs(token, now) {
|
|
248
|
+
const exp = claims(token).exp;
|
|
249
|
+
return typeof exp === "number" ? exp * 1e3 : now + DEFAULT_TOKEN_LIFETIME_SECONDS * 1e3;
|
|
250
|
+
}
|
|
251
|
+
function tokenLifetimeSeconds(token) {
|
|
252
|
+
const { exp, iat } = claims(token);
|
|
253
|
+
return typeof exp === "number" && typeof iat === "number" && exp > iat ? exp - iat : DEFAULT_TOKEN_LIFETIME_SECONDS;
|
|
254
|
+
}
|
|
255
|
+
function claims(token) {
|
|
256
|
+
try {
|
|
257
|
+
return (0, import_jose2.decodeJwt)(token);
|
|
258
|
+
} catch {
|
|
259
|
+
return {};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
107
263
|
// src/logging.ts
|
|
108
264
|
function logToolCall(tool, code, startedAt, replayedGrants) {
|
|
109
265
|
process.stderr.write(`${JSON.stringify({
|
|
@@ -116,7 +272,7 @@ function logToolCall(tool, code, startedAt, replayedGrants) {
|
|
|
116
272
|
}
|
|
117
273
|
|
|
118
274
|
// src/remote-store.ts
|
|
119
|
-
var
|
|
275
|
+
var import_node_crypto2 = require("crypto");
|
|
120
276
|
var import_promises = require("fs/promises");
|
|
121
277
|
var import_node_path = require("path");
|
|
122
278
|
var import_node_sdk2 = require("@tinycloud/node-sdk");
|
|
@@ -708,7 +864,7 @@ var RemoteTenantStore = class {
|
|
|
708
864
|
{ tenantId, approvalEpoch: tenant.approvalEpoch, kind, requestId, permissions, ownerDids }
|
|
709
865
|
);
|
|
710
866
|
if (reusable !== void 0) return reusable;
|
|
711
|
-
const nonce = (0,
|
|
867
|
+
const nonce = (0, import_node_crypto2.randomBytes)(24).toString("base64url");
|
|
712
868
|
const created = Date.now();
|
|
713
869
|
const pending = {
|
|
714
870
|
version: 1,
|
|
@@ -733,7 +889,7 @@ var RemoteTenantStore = class {
|
|
|
733
889
|
const expected = this.#signState(tenantId, nonce);
|
|
734
890
|
const supplied = Buffer.from(signature, "base64url");
|
|
735
891
|
const expectedBytes = Buffer.from(expected, "base64url");
|
|
736
|
-
if (supplied.length !== expectedBytes.length || !(0,
|
|
892
|
+
if (supplied.length !== expectedBytes.length || !(0, import_node_crypto2.timingSafeEqual)(supplied, expectedBytes)) {
|
|
737
893
|
throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
738
894
|
}
|
|
739
895
|
const stateRoot = (0, import_node_path.join)(this.#config.stateDir, "tenants", tenantId);
|
|
@@ -785,10 +941,10 @@ var RemoteTenantStore = class {
|
|
|
785
941
|
return url.toString();
|
|
786
942
|
}
|
|
787
943
|
#tenantId(subject) {
|
|
788
|
-
return (0,
|
|
944
|
+
return (0, import_node_crypto2.createHmac)("sha256", this.#config.stateSecret).update(`tenant:${subject}`).digest("hex");
|
|
789
945
|
}
|
|
790
946
|
#signState(tenantId, nonce) {
|
|
791
|
-
return (0,
|
|
947
|
+
return (0, import_node_crypto2.createHmac)("sha256", this.#config.stateSecret).update(`approval:${tenantId}:${nonce}`).digest("base64url");
|
|
792
948
|
}
|
|
793
949
|
#profilePath(stateRoot) {
|
|
794
950
|
return (0, import_node_path.join)(stateRoot, ".tinycloud", "profiles", PROFILE, "profile.json");
|
|
@@ -890,7 +1046,7 @@ async function secureDirectory(path) {
|
|
|
890
1046
|
async function secureWrite(path, value) {
|
|
891
1047
|
const directory = (0, import_node_path.dirname)(path);
|
|
892
1048
|
await secureDirectory(directory);
|
|
893
|
-
const temporary = `${path}.${process.pid}.${(0,
|
|
1049
|
+
const temporary = `${path}.${process.pid}.${(0, import_node_crypto2.randomBytes)(8).toString("hex")}.tmp`;
|
|
894
1050
|
await (0, import_promises.writeFile)(temporary, `${JSON.stringify(value, jsonReplacer, 2)}
|
|
895
1051
|
`, { mode: 384 });
|
|
896
1052
|
await (0, import_promises.rename)(temporary, path);
|
|
@@ -1382,7 +1538,7 @@ function outcomeCode(result) {
|
|
|
1382
1538
|
// package.json
|
|
1383
1539
|
var package_default = {
|
|
1384
1540
|
name: "@tinycloud/mcp",
|
|
1385
|
-
version: "0.8.3-beta.
|
|
1541
|
+
version: "0.8.3-beta.8",
|
|
1386
1542
|
description: "TinyCloud delegated operations MCP server",
|
|
1387
1543
|
keywords: [
|
|
1388
1544
|
"tinycloud",
|
|
@@ -1482,12 +1638,13 @@ var REQUIRED_SCOPE = "tinycloud:mcp";
|
|
|
1482
1638
|
function createHostedMcpApp(config) {
|
|
1483
1639
|
const resourceUrl = canonicalResourceUrl(config.publicUrl);
|
|
1484
1640
|
const pluginUrl = new URL("./chatgpt/mcp", resourceUrl);
|
|
1485
|
-
const
|
|
1641
|
+
const statusFor = (resource) => config.introspection === void 0 ? void 0 : createTokenStatusChecker({ ...config.introspection, resourceUrl: resource.toString() });
|
|
1642
|
+
const hosted = createHostedSurface(config, resourceUrl, "hosted", statusFor(resourceUrl));
|
|
1486
1643
|
const plugin = createHostedSurface({
|
|
1487
1644
|
...config,
|
|
1488
1645
|
stateDir: (0, import_node_path3.join)(config.stateDir, "chatgpt"),
|
|
1489
1646
|
approvalPath: new URL("./connect", pluginUrl).pathname
|
|
1490
|
-
}, pluginUrl, "public");
|
|
1647
|
+
}, pluginUrl, "public", statusFor(pluginUrl));
|
|
1491
1648
|
const pluginPaths = {
|
|
1492
1649
|
[pluginUrl.pathname]: true,
|
|
1493
1650
|
[new URL("./connect", pluginUrl).pathname]: true,
|
|
@@ -1501,7 +1658,7 @@ function createHostedMcpApp(config) {
|
|
|
1501
1658
|
}
|
|
1502
1659
|
};
|
|
1503
1660
|
}
|
|
1504
|
-
function createHostedSurface(config, resourceUrl, surface) {
|
|
1661
|
+
function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
|
|
1505
1662
|
const connectPath = config.approvalPath ?? "/connect";
|
|
1506
1663
|
const callbackPath = `${connectPath}/callback`;
|
|
1507
1664
|
const issuer = String(config.oauthMetadata.issuer);
|
|
@@ -1510,7 +1667,8 @@ function createHostedSurface(config, resourceUrl, surface) {
|
|
|
1510
1667
|
issuer,
|
|
1511
1668
|
audience: resourceUrl.toString(),
|
|
1512
1669
|
jwksUri,
|
|
1513
|
-
requiredScope: REQUIRED_SCOPE
|
|
1670
|
+
requiredScope: REQUIRED_SCOPE,
|
|
1671
|
+
...tokenStatus === void 0 ? {} : { tokenStatus }
|
|
1514
1672
|
});
|
|
1515
1673
|
const resourceMetadataUrl = (0, import_server4.getOAuthProtectedResourceMetadataUrl)(resourceUrl);
|
|
1516
1674
|
const authenticate = (0, import_server4.requireBearerAuth)({
|