@tinycloud/mcp 0.5.4-beta.0 → 0.5.4-beta.1
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/dist/cli.cjs +2 -2
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/http-cli.cjs +121 -29
- package/dist/http-cli.cjs.map +1 -1
- package/dist/http-cli.js +124 -29
- package/dist/http-cli.js.map +1 -1
- package/dist/http.cjs +121 -29
- package/dist/http.cjs.map +1 -1
- package/dist/http.js +124 -29
- package/dist/http.js.map +1 -1
- package/dist/index.cjs +121 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +124 -29
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +2 -2
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +2 -2
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
package/dist/http-cli.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
oauthMetadataResponse,
|
|
14
14
|
requireBearerAuth
|
|
15
15
|
} from "@modelcontextprotocol/server";
|
|
16
|
+
import { RUNTIME_DELEGATION_REJECTIONS } from "@tinycloud/node-sdk";
|
|
16
17
|
|
|
17
18
|
// src/oauth.ts
|
|
18
19
|
import {
|
|
@@ -111,7 +112,10 @@ import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
|
111
112
|
import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
|
|
112
113
|
import { dirname, join } from "path";
|
|
113
114
|
import {
|
|
115
|
+
InvalidRestoredSessionError,
|
|
116
|
+
RuntimeDelegationRejectedError,
|
|
114
117
|
TinyCloudNode,
|
|
118
|
+
UnsupportedSessionRestoreError,
|
|
115
119
|
isCapabilitySubset,
|
|
116
120
|
principalDidEquals,
|
|
117
121
|
siweRuntimeDelegation
|
|
@@ -140,6 +144,26 @@ var ApprovalOwnerMismatchError = class extends Error {
|
|
|
140
144
|
this.name = "ApprovalOwnerMismatchError";
|
|
141
145
|
}
|
|
142
146
|
};
|
|
147
|
+
var ApprovalStageError = class extends Error {
|
|
148
|
+
stage;
|
|
149
|
+
code;
|
|
150
|
+
reason;
|
|
151
|
+
constructor(stage, code, reason, cause) {
|
|
152
|
+
super(`The approval failed at ${stage} (${code}).`, { cause });
|
|
153
|
+
this.name = "ApprovalStageError";
|
|
154
|
+
this.stage = stage;
|
|
155
|
+
this.code = code;
|
|
156
|
+
this.reason = reason;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
var ApprovalRequestError = class extends Error {
|
|
160
|
+
detail;
|
|
161
|
+
constructor(detail, message, cause) {
|
|
162
|
+
super(message, { cause });
|
|
163
|
+
this.name = "ApprovalRequestError";
|
|
164
|
+
this.detail = detail;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
143
167
|
var RemoteTenantStore = class {
|
|
144
168
|
#config;
|
|
145
169
|
#initializing = /* @__PURE__ */ new Map();
|
|
@@ -205,11 +229,20 @@ var RemoteTenantStore = class {
|
|
|
205
229
|
}
|
|
206
230
|
async completeApproval(state, request) {
|
|
207
231
|
const length = Number(request.headers.get("content-length") ?? "0");
|
|
208
|
-
if (Number.isFinite(length) && length > MAX_CALLBACK_BYTES)
|
|
232
|
+
if (Number.isFinite(length) && length > MAX_CALLBACK_BYTES) {
|
|
233
|
+
throw new ApprovalRequestError("body_invalid", "Callback body is too large.");
|
|
234
|
+
}
|
|
209
235
|
const text = await request.text();
|
|
210
|
-
if (Buffer.byteLength(text) > MAX_CALLBACK_BYTES)
|
|
211
|
-
|
|
212
|
-
|
|
236
|
+
if (Buffer.byteLength(text) > MAX_CALLBACK_BYTES) {
|
|
237
|
+
throw new ApprovalRequestError("body_invalid", "Callback body is too large.");
|
|
238
|
+
}
|
|
239
|
+
let data;
|
|
240
|
+
try {
|
|
241
|
+
data = JSON.parse(text);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
throw new ApprovalRequestError("body_invalid", "Callback body is invalid.", error);
|
|
244
|
+
}
|
|
245
|
+
if (!isRecord(data)) throw new ApprovalRequestError("body_invalid", "Callback body is invalid.");
|
|
213
246
|
const pending = await this.#readPending(state, true);
|
|
214
247
|
try {
|
|
215
248
|
const tenant = await this.#readTenant(pending.tenantId);
|
|
@@ -247,16 +280,26 @@ var RemoteTenantStore = class {
|
|
|
247
280
|
if (pending.requestId === void 0) throw new Error("Authority request is missing.");
|
|
248
281
|
const { node, session } = await this.#restoreApproval(tenant, data);
|
|
249
282
|
if (!pending.ownerDids.some((did) => samePrincipal(node.did, did))) throw new ApprovalOwnerMismatchError();
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
283
|
+
let delegation;
|
|
284
|
+
try {
|
|
285
|
+
delegation = siweRuntimeDelegation(node, {
|
|
286
|
+
authorization: session.delegationHeader.Authorization,
|
|
287
|
+
cid: session.delegationCid,
|
|
288
|
+
spaceId: session.spaceId,
|
|
289
|
+
ownerAddress: session.address,
|
|
290
|
+
chainId: session.chainId,
|
|
291
|
+
siwe: session.siwe,
|
|
292
|
+
signature: session.signature,
|
|
293
|
+
host: this.#config.nodeHost
|
|
294
|
+
});
|
|
295
|
+
} catch (error) {
|
|
296
|
+
throw new ApprovalStageError(
|
|
297
|
+
"siwe_runtime_delegation",
|
|
298
|
+
"DELEGATION_REJECTED",
|
|
299
|
+
error instanceof RuntimeDelegationRejectedError ? error.reason : void 0,
|
|
300
|
+
error
|
|
301
|
+
);
|
|
302
|
+
}
|
|
260
303
|
const result = await invokeOperation(
|
|
261
304
|
"tinycloud.auth.import",
|
|
262
305
|
1,
|
|
@@ -269,7 +312,15 @@ var RemoteTenantStore = class {
|
|
|
269
312
|
delegation
|
|
270
313
|
}
|
|
271
314
|
);
|
|
272
|
-
if (result.status
|
|
315
|
+
if (result.status === "error") {
|
|
316
|
+
const reason = result.error.details?.reason;
|
|
317
|
+
throw new ApprovalStageError(
|
|
318
|
+
result.error.code === "DELEGATION_REJECTED" && typeof reason === "string" ? "activation" : "auth_import",
|
|
319
|
+
result.error.code,
|
|
320
|
+
typeof reason === "string" ? reason : void 0
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
if (result.status !== "ok") throw new ApprovalStageError("auth_import", "AUTHORITY_REQUIRED");
|
|
273
324
|
}
|
|
274
325
|
/**
|
|
275
326
|
* Verify the OpenKey callback as a SIWE session for this tenant's key. The
|
|
@@ -279,11 +330,25 @@ var RemoteTenantStore = class {
|
|
|
279
330
|
* exactly the delegation being stored.
|
|
280
331
|
*/
|
|
281
332
|
async #restoreApproval(tenant, data) {
|
|
282
|
-
|
|
333
|
+
let session;
|
|
334
|
+
try {
|
|
335
|
+
session = sessionFromCallback(data, tenant.jwk);
|
|
336
|
+
} catch (error) {
|
|
337
|
+
throw new ApprovalStageError("restore_approval", "CALLBACK_INCOMPLETE", void 0, error);
|
|
338
|
+
}
|
|
283
339
|
const node = new TinyCloudNode({ host: this.#config.nodeHost });
|
|
284
|
-
|
|
340
|
+
try {
|
|
341
|
+
await node.restoreSession(session);
|
|
342
|
+
} catch (error) {
|
|
343
|
+
throw new ApprovalStageError(
|
|
344
|
+
"restore_approval",
|
|
345
|
+
error instanceof InvalidRestoredSessionError || error instanceof UnsupportedSessionRestoreError ? error.code : "SESSION_RESTORE_FAILED",
|
|
346
|
+
void 0,
|
|
347
|
+
error
|
|
348
|
+
);
|
|
349
|
+
}
|
|
285
350
|
if (!samePrincipal(node.sessionDid, tenant.sessionDid)) {
|
|
286
|
-
throw new
|
|
351
|
+
throw new ApprovalStageError("restore_approval", "SESSION_MISMATCH");
|
|
287
352
|
}
|
|
288
353
|
return { node, session };
|
|
289
354
|
}
|
|
@@ -342,10 +407,10 @@ var RemoteTenantStore = class {
|
|
|
342
407
|
return { stateRoot, sessionDid, jwk };
|
|
343
408
|
}
|
|
344
409
|
async #readTenant(tenantId) {
|
|
345
|
-
if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new
|
|
410
|
+
if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
346
411
|
const stateRoot = join(this.#config.stateDir, "tenants", tenantId);
|
|
347
412
|
const jwk = await readJson(this.#keyPath(stateRoot));
|
|
348
|
-
if (jwk === null) throw new
|
|
413
|
+
if (jwk === null) throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.");
|
|
349
414
|
return { stateRoot, jwk, sessionDid: didFromJwk(jwk) };
|
|
350
415
|
}
|
|
351
416
|
async #createPending(subject, ownerDids, kind, permissions, requestId) {
|
|
@@ -375,26 +440,40 @@ var RemoteTenantStore = class {
|
|
|
375
440
|
async #readPending(state, consume) {
|
|
376
441
|
const [tenantId, nonce, signature] = state.split(".");
|
|
377
442
|
if (tenantId === void 0 || nonce === void 0 || signature === void 0) {
|
|
378
|
-
throw new
|
|
443
|
+
throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
379
444
|
}
|
|
380
445
|
const expected = this.#signState(tenantId, nonce);
|
|
381
446
|
const supplied = Buffer.from(signature, "base64url");
|
|
382
447
|
const expectedBytes = Buffer.from(expected, "base64url");
|
|
383
448
|
if (supplied.length !== expectedBytes.length || !timingSafeEqual(supplied, expectedBytes)) {
|
|
384
|
-
throw new
|
|
449
|
+
throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
385
450
|
}
|
|
386
451
|
const stateRoot = join(this.#config.stateDir, "tenants", tenantId);
|
|
387
452
|
const path = this.#pendingPath(stateRoot, nonce);
|
|
388
453
|
const claimed = `${path}.processing`;
|
|
389
|
-
if (consume)
|
|
454
|
+
if (consume) {
|
|
455
|
+
try {
|
|
456
|
+
await rename(path, claimed);
|
|
457
|
+
} catch (error) {
|
|
458
|
+
if (!isRecord(error) || error.code !== "ENOENT") throw error;
|
|
459
|
+
const tenantExists = await readJson(this.#keyPath(stateRoot)).then(
|
|
460
|
+
(jwk) => jwk !== null,
|
|
461
|
+
() => true
|
|
462
|
+
);
|
|
463
|
+
if (!tenantExists) {
|
|
464
|
+
throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.", error);
|
|
465
|
+
}
|
|
466
|
+
throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.", error);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
390
469
|
try {
|
|
391
470
|
const pending = await readJson(consume ? claimed : path);
|
|
392
471
|
if (pending === null || pending.version !== 1 || pending.tenantId !== tenantId || pending.nonce !== nonce) {
|
|
393
|
-
throw new
|
|
472
|
+
throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
|
|
394
473
|
}
|
|
395
474
|
if (new Date(pending.expiresAt).getTime() <= Date.now()) {
|
|
396
475
|
await rm(consume ? claimed : path, { force: true });
|
|
397
|
-
throw new
|
|
476
|
+
throw new ApprovalRequestError("state_expired", "Approval state has expired.");
|
|
398
477
|
}
|
|
399
478
|
return pending;
|
|
400
479
|
} catch (error) {
|
|
@@ -841,7 +920,7 @@ function registerTinyCloudTools(server, selection, validator) {
|
|
|
841
920
|
// package.json
|
|
842
921
|
var package_default = {
|
|
843
922
|
name: "@tinycloud/mcp",
|
|
844
|
-
version: "0.5.4-beta.
|
|
923
|
+
version: "0.5.4-beta.1",
|
|
845
924
|
description: "TinyCloud delegated operations MCP server",
|
|
846
925
|
keywords: [
|
|
847
926
|
"tinycloud",
|
|
@@ -914,7 +993,7 @@ var package_default = {
|
|
|
914
993
|
"@modelcontextprotocol/server": "2.0.0-beta.4",
|
|
915
994
|
"@tinycloud/node-sdk": "3.2.4-beta.0",
|
|
916
995
|
"@tinycloud/node-sdk-wasm": "1.7.7",
|
|
917
|
-
"@tinycloud/operations": "0.5.4-beta.
|
|
996
|
+
"@tinycloud/operations": "0.5.4-beta.1",
|
|
918
997
|
jose: "6.1.3"
|
|
919
998
|
},
|
|
920
999
|
devDependencies: {
|
|
@@ -1017,8 +1096,23 @@ function createHostedMcpApp(config) {
|
|
|
1017
1096
|
await store.completeApproval(requiredState(url), request);
|
|
1018
1097
|
return callbackCors(json({ success: true }), config.openkeyHost);
|
|
1019
1098
|
} catch (error) {
|
|
1099
|
+
if (error instanceof ApprovalOwnerMismatchError) {
|
|
1100
|
+
return callbackCors(json({ error: error.message, code: "approval_owner_mismatch" }, 403), config.openkeyHost);
|
|
1101
|
+
}
|
|
1102
|
+
const stage = error instanceof ApprovalStageError ? error.stage : "unknown";
|
|
1103
|
+
const code = error instanceof ApprovalStageError && /^[A-Z][A-Z0-9_]{0,63}$/.test(error.code) ? error.code : "UNKNOWN";
|
|
1104
|
+
const reason = !(error instanceof ApprovalStageError) || error.reason === void 0 ? void 0 : RUNTIME_DELEGATION_REJECTIONS.find((known) => known === error.reason) ?? "unknown";
|
|
1105
|
+
const detail = error instanceof ApprovalRequestError ? error.detail : "none";
|
|
1106
|
+
writeDiagnostic(
|
|
1107
|
+
`connect callback rejected: stage=${stage} code=${code}${reason === void 0 ? "" : ` reason=${reason}`} detail=${detail}`
|
|
1108
|
+
);
|
|
1020
1109
|
return callbackCors(
|
|
1021
|
-
|
|
1110
|
+
json({
|
|
1111
|
+
error: stage,
|
|
1112
|
+
code,
|
|
1113
|
+
...reason === void 0 ? {} : { reason },
|
|
1114
|
+
message: "TinyCloud could not accept this delegation."
|
|
1115
|
+
}, 400),
|
|
1022
1116
|
config.openkeyHost
|
|
1023
1117
|
);
|
|
1024
1118
|
}
|
|
@@ -1093,7 +1187,8 @@ function canonicalResourceUrl(url) {
|
|
|
1093
1187
|
}
|
|
1094
1188
|
function requiredState(url) {
|
|
1095
1189
|
const state = url.searchParams.get("state");
|
|
1096
|
-
if (state === null
|
|
1190
|
+
if (state === null) throw new ApprovalRequestError("state_missing", "Missing approval state.");
|
|
1191
|
+
if (state.length > 512) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
1097
1192
|
return state;
|
|
1098
1193
|
}
|
|
1099
1194
|
function validateOrigin(request, allowed) {
|