@tinycloud/mcp 0.5.4-beta.0 → 0.5.4-beta.2
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 +130 -33
- package/dist/http-cli.cjs.map +1 -1
- package/dist/http-cli.js +133 -33
- package/dist/http-cli.js.map +1 -1
- package/dist/http.cjs +130 -33
- package/dist/http.cjs.map +1 -1
- package/dist/http.js +133 -33
- package/dist/http.js.map +1 -1
- package/dist/index.cjs +130 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +133 -33
- 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
|
|
@@ -120,6 +124,7 @@ import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk
|
|
|
120
124
|
import { invokeOperation } from "@tinycloud/operations";
|
|
121
125
|
var PROFILE = "agent";
|
|
122
126
|
var MAX_CALLBACK_BYTES = 256 * 1024;
|
|
127
|
+
var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
|
|
123
128
|
var BOOTSTRAP_PERMISSIONS = [
|
|
124
129
|
{
|
|
125
130
|
service: "tinycloud.kv",
|
|
@@ -140,6 +145,26 @@ var ApprovalOwnerMismatchError = class extends Error {
|
|
|
140
145
|
this.name = "ApprovalOwnerMismatchError";
|
|
141
146
|
}
|
|
142
147
|
};
|
|
148
|
+
var ApprovalStageError = class extends Error {
|
|
149
|
+
stage;
|
|
150
|
+
code;
|
|
151
|
+
reason;
|
|
152
|
+
constructor(stage, code, reason, cause) {
|
|
153
|
+
super(`The approval failed at ${stage} (${code}).`, { cause });
|
|
154
|
+
this.name = "ApprovalStageError";
|
|
155
|
+
this.stage = stage;
|
|
156
|
+
this.code = code;
|
|
157
|
+
this.reason = reason;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
var ApprovalRequestError = class extends Error {
|
|
161
|
+
detail;
|
|
162
|
+
constructor(detail, message, cause) {
|
|
163
|
+
super(message, { cause });
|
|
164
|
+
this.name = "ApprovalRequestError";
|
|
165
|
+
this.detail = detail;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
143
168
|
var RemoteTenantStore = class {
|
|
144
169
|
#config;
|
|
145
170
|
#initializing = /* @__PURE__ */ new Map();
|
|
@@ -205,11 +230,20 @@ var RemoteTenantStore = class {
|
|
|
205
230
|
}
|
|
206
231
|
async completeApproval(state, request) {
|
|
207
232
|
const length = Number(request.headers.get("content-length") ?? "0");
|
|
208
|
-
if (Number.isFinite(length) && length > MAX_CALLBACK_BYTES)
|
|
233
|
+
if (Number.isFinite(length) && length > MAX_CALLBACK_BYTES) {
|
|
234
|
+
throw new ApprovalRequestError("body_invalid", "Callback body is too large.");
|
|
235
|
+
}
|
|
209
236
|
const text = await request.text();
|
|
210
|
-
if (Buffer.byteLength(text) > MAX_CALLBACK_BYTES)
|
|
211
|
-
|
|
212
|
-
|
|
237
|
+
if (Buffer.byteLength(text) > MAX_CALLBACK_BYTES) {
|
|
238
|
+
throw new ApprovalRequestError("body_invalid", "Callback body is too large.");
|
|
239
|
+
}
|
|
240
|
+
let data;
|
|
241
|
+
try {
|
|
242
|
+
data = JSON.parse(text);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
throw new ApprovalRequestError("body_invalid", "Callback body is invalid.", error);
|
|
245
|
+
}
|
|
246
|
+
if (!isRecord(data)) throw new ApprovalRequestError("body_invalid", "Callback body is invalid.");
|
|
213
247
|
const pending = await this.#readPending(state, true);
|
|
214
248
|
try {
|
|
215
249
|
const tenant = await this.#readTenant(pending.tenantId);
|
|
@@ -247,16 +281,26 @@ var RemoteTenantStore = class {
|
|
|
247
281
|
if (pending.requestId === void 0) throw new Error("Authority request is missing.");
|
|
248
282
|
const { node, session } = await this.#restoreApproval(tenant, data);
|
|
249
283
|
if (!pending.ownerDids.some((did) => samePrincipal(node.did, did))) throw new ApprovalOwnerMismatchError();
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
284
|
+
let delegation;
|
|
285
|
+
try {
|
|
286
|
+
delegation = siweRuntimeDelegation(node, {
|
|
287
|
+
authorization: session.delegationHeader.Authorization,
|
|
288
|
+
cid: session.delegationCid,
|
|
289
|
+
spaceId: session.spaceId,
|
|
290
|
+
ownerAddress: session.address,
|
|
291
|
+
chainId: session.chainId,
|
|
292
|
+
siwe: session.siwe,
|
|
293
|
+
signature: session.signature,
|
|
294
|
+
host: this.#config.nodeHost
|
|
295
|
+
});
|
|
296
|
+
} catch (error) {
|
|
297
|
+
throw new ApprovalStageError(
|
|
298
|
+
"siwe_runtime_delegation",
|
|
299
|
+
"DELEGATION_REJECTED",
|
|
300
|
+
error instanceof RuntimeDelegationRejectedError ? error.reason : void 0,
|
|
301
|
+
error
|
|
302
|
+
);
|
|
303
|
+
}
|
|
260
304
|
const result = await invokeOperation(
|
|
261
305
|
"tinycloud.auth.import",
|
|
262
306
|
1,
|
|
@@ -269,7 +313,15 @@ var RemoteTenantStore = class {
|
|
|
269
313
|
delegation
|
|
270
314
|
}
|
|
271
315
|
);
|
|
272
|
-
if (result.status
|
|
316
|
+
if (result.status === "error") {
|
|
317
|
+
const reason = result.error.details?.reason;
|
|
318
|
+
throw new ApprovalStageError(
|
|
319
|
+
result.error.code === "DELEGATION_REJECTED" && typeof reason === "string" ? "activation" : "auth_import",
|
|
320
|
+
result.error.code,
|
|
321
|
+
typeof reason === "string" ? reason : void 0
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
if (result.status !== "ok") throw new ApprovalStageError("auth_import", "AUTHORITY_REQUIRED");
|
|
273
325
|
}
|
|
274
326
|
/**
|
|
275
327
|
* Verify the OpenKey callback as a SIWE session for this tenant's key. The
|
|
@@ -279,11 +331,25 @@ var RemoteTenantStore = class {
|
|
|
279
331
|
* exactly the delegation being stored.
|
|
280
332
|
*/
|
|
281
333
|
async #restoreApproval(tenant, data) {
|
|
282
|
-
|
|
334
|
+
let session;
|
|
335
|
+
try {
|
|
336
|
+
session = sessionFromCallback(data, tenant.jwk);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
throw new ApprovalStageError("restore_approval", "CALLBACK_INCOMPLETE", void 0, error);
|
|
339
|
+
}
|
|
283
340
|
const node = new TinyCloudNode({ host: this.#config.nodeHost });
|
|
284
|
-
|
|
341
|
+
try {
|
|
342
|
+
await node.restoreSession(session);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
throw new ApprovalStageError(
|
|
345
|
+
"restore_approval",
|
|
346
|
+
error instanceof InvalidRestoredSessionError || error instanceof UnsupportedSessionRestoreError ? error.code : "SESSION_RESTORE_FAILED",
|
|
347
|
+
void 0,
|
|
348
|
+
error
|
|
349
|
+
);
|
|
350
|
+
}
|
|
285
351
|
if (!samePrincipal(node.sessionDid, tenant.sessionDid)) {
|
|
286
|
-
throw new
|
|
352
|
+
throw new ApprovalStageError("restore_approval", "SESSION_MISMATCH");
|
|
287
353
|
}
|
|
288
354
|
return { node, session };
|
|
289
355
|
}
|
|
@@ -324,7 +390,8 @@ var RemoteTenantStore = class {
|
|
|
324
390
|
sessionDid = didFromJwk(jwk);
|
|
325
391
|
}
|
|
326
392
|
const profilePath = this.#profilePath(stateRoot);
|
|
327
|
-
|
|
393
|
+
const profile = await readJson(profilePath);
|
|
394
|
+
if (profile === null) {
|
|
328
395
|
await secureWrite(profilePath, {
|
|
329
396
|
name: PROFILE,
|
|
330
397
|
host: this.#config.nodeHost,
|
|
@@ -338,14 +405,20 @@ var RemoteTenantStore = class {
|
|
|
338
405
|
authMethod: "openkey",
|
|
339
406
|
openkeyHost: this.#config.openkeyHost
|
|
340
407
|
});
|
|
408
|
+
} else if (typeof profile.host === "string" && profile.host.replace(/\/+$/, "") === LEGACY_DEFAULT_NODE_HOST && this.#config.nodeHost.replace(/\/+$/, "") !== LEGACY_DEFAULT_NODE_HOST) {
|
|
409
|
+
await secureWrite(profilePath, { ...profile, host: this.#config.nodeHost });
|
|
410
|
+
process.stderr.write(
|
|
411
|
+
`[tinycloud-mcp-http] tenant node host migrated: from=${LEGACY_DEFAULT_NODE_HOST} to=${new URL(this.#config.nodeHost).origin}
|
|
412
|
+
`
|
|
413
|
+
);
|
|
341
414
|
}
|
|
342
415
|
return { stateRoot, sessionDid, jwk };
|
|
343
416
|
}
|
|
344
417
|
async #readTenant(tenantId) {
|
|
345
|
-
if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new
|
|
418
|
+
if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
346
419
|
const stateRoot = join(this.#config.stateDir, "tenants", tenantId);
|
|
347
420
|
const jwk = await readJson(this.#keyPath(stateRoot));
|
|
348
|
-
if (jwk === null) throw new
|
|
421
|
+
if (jwk === null) throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.");
|
|
349
422
|
return { stateRoot, jwk, sessionDid: didFromJwk(jwk) };
|
|
350
423
|
}
|
|
351
424
|
async #createPending(subject, ownerDids, kind, permissions, requestId) {
|
|
@@ -375,26 +448,40 @@ var RemoteTenantStore = class {
|
|
|
375
448
|
async #readPending(state, consume) {
|
|
376
449
|
const [tenantId, nonce, signature] = state.split(".");
|
|
377
450
|
if (tenantId === void 0 || nonce === void 0 || signature === void 0) {
|
|
378
|
-
throw new
|
|
451
|
+
throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
379
452
|
}
|
|
380
453
|
const expected = this.#signState(tenantId, nonce);
|
|
381
454
|
const supplied = Buffer.from(signature, "base64url");
|
|
382
455
|
const expectedBytes = Buffer.from(expected, "base64url");
|
|
383
456
|
if (supplied.length !== expectedBytes.length || !timingSafeEqual(supplied, expectedBytes)) {
|
|
384
|
-
throw new
|
|
457
|
+
throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
385
458
|
}
|
|
386
459
|
const stateRoot = join(this.#config.stateDir, "tenants", tenantId);
|
|
387
460
|
const path = this.#pendingPath(stateRoot, nonce);
|
|
388
461
|
const claimed = `${path}.processing`;
|
|
389
|
-
if (consume)
|
|
462
|
+
if (consume) {
|
|
463
|
+
try {
|
|
464
|
+
await rename(path, claimed);
|
|
465
|
+
} catch (error) {
|
|
466
|
+
if (!isRecord(error) || error.code !== "ENOENT") throw error;
|
|
467
|
+
const tenantExists = await readJson(this.#keyPath(stateRoot)).then(
|
|
468
|
+
(jwk) => jwk !== null,
|
|
469
|
+
() => true
|
|
470
|
+
);
|
|
471
|
+
if (!tenantExists) {
|
|
472
|
+
throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.", error);
|
|
473
|
+
}
|
|
474
|
+
throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.", error);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
390
477
|
try {
|
|
391
478
|
const pending = await readJson(consume ? claimed : path);
|
|
392
479
|
if (pending === null || pending.version !== 1 || pending.tenantId !== tenantId || pending.nonce !== nonce) {
|
|
393
|
-
throw new
|
|
480
|
+
throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
|
|
394
481
|
}
|
|
395
482
|
if (new Date(pending.expiresAt).getTime() <= Date.now()) {
|
|
396
483
|
await rm(consume ? claimed : path, { force: true });
|
|
397
|
-
throw new
|
|
484
|
+
throw new ApprovalRequestError("state_expired", "Approval state has expired.");
|
|
398
485
|
}
|
|
399
486
|
return pending;
|
|
400
487
|
} catch (error) {
|
|
@@ -517,9 +604,6 @@ async function readJson(path) {
|
|
|
517
604
|
throw error;
|
|
518
605
|
}
|
|
519
606
|
}
|
|
520
|
-
async function fileExists(path) {
|
|
521
|
-
return await readJson(path) !== null;
|
|
522
|
-
}
|
|
523
607
|
async function reusablePending(directory, expected) {
|
|
524
608
|
let names;
|
|
525
609
|
try {
|
|
@@ -841,7 +925,7 @@ function registerTinyCloudTools(server, selection, validator) {
|
|
|
841
925
|
// package.json
|
|
842
926
|
var package_default = {
|
|
843
927
|
name: "@tinycloud/mcp",
|
|
844
|
-
version: "0.5.4-beta.
|
|
928
|
+
version: "0.5.4-beta.2",
|
|
845
929
|
description: "TinyCloud delegated operations MCP server",
|
|
846
930
|
keywords: [
|
|
847
931
|
"tinycloud",
|
|
@@ -914,7 +998,7 @@ var package_default = {
|
|
|
914
998
|
"@modelcontextprotocol/server": "2.0.0-beta.4",
|
|
915
999
|
"@tinycloud/node-sdk": "3.2.4-beta.0",
|
|
916
1000
|
"@tinycloud/node-sdk-wasm": "1.7.7",
|
|
917
|
-
"@tinycloud/operations": "0.5.4-beta.
|
|
1001
|
+
"@tinycloud/operations": "0.5.4-beta.1",
|
|
918
1002
|
jose: "6.1.3"
|
|
919
1003
|
},
|
|
920
1004
|
devDependencies: {
|
|
@@ -1017,8 +1101,23 @@ function createHostedMcpApp(config) {
|
|
|
1017
1101
|
await store.completeApproval(requiredState(url), request);
|
|
1018
1102
|
return callbackCors(json({ success: true }), config.openkeyHost);
|
|
1019
1103
|
} catch (error) {
|
|
1104
|
+
if (error instanceof ApprovalOwnerMismatchError) {
|
|
1105
|
+
return callbackCors(json({ error: error.message, code: "approval_owner_mismatch" }, 403), config.openkeyHost);
|
|
1106
|
+
}
|
|
1107
|
+
const stage = error instanceof ApprovalStageError ? error.stage : "unknown";
|
|
1108
|
+
const code = error instanceof ApprovalStageError && /^[A-Z][A-Z0-9_]{0,63}$/.test(error.code) ? error.code : "UNKNOWN";
|
|
1109
|
+
const reason = !(error instanceof ApprovalStageError) || error.reason === void 0 ? void 0 : RUNTIME_DELEGATION_REJECTIONS.find((known) => known === error.reason) ?? "unknown";
|
|
1110
|
+
const detail = error instanceof ApprovalRequestError ? error.detail : "none";
|
|
1111
|
+
writeDiagnostic(
|
|
1112
|
+
`connect callback rejected: stage=${stage} code=${code}${reason === void 0 ? "" : ` reason=${reason}`} detail=${detail}`
|
|
1113
|
+
);
|
|
1020
1114
|
return callbackCors(
|
|
1021
|
-
|
|
1115
|
+
json({
|
|
1116
|
+
error: stage,
|
|
1117
|
+
code,
|
|
1118
|
+
...reason === void 0 ? {} : { reason },
|
|
1119
|
+
message: "TinyCloud could not accept this delegation."
|
|
1120
|
+
}, 400),
|
|
1022
1121
|
config.openkeyHost
|
|
1023
1122
|
);
|
|
1024
1123
|
}
|
|
@@ -1093,7 +1192,8 @@ function canonicalResourceUrl(url) {
|
|
|
1093
1192
|
}
|
|
1094
1193
|
function requiredState(url) {
|
|
1095
1194
|
const state = url.searchParams.get("state");
|
|
1096
|
-
if (state === null
|
|
1195
|
+
if (state === null) throw new ApprovalRequestError("state_missing", "Missing approval state.");
|
|
1196
|
+
if (state.length > 512) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
1097
1197
|
return state;
|
|
1098
1198
|
}
|
|
1099
1199
|
function validateOrigin(request, allowed) {
|