@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.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
oauthMetadataResponse,
|
|
9
9
|
requireBearerAuth
|
|
10
10
|
} from "@modelcontextprotocol/server";
|
|
11
|
+
import { RUNTIME_DELEGATION_REJECTIONS } from "@tinycloud/node-sdk";
|
|
11
12
|
|
|
12
13
|
// src/oauth.ts
|
|
13
14
|
import {
|
|
@@ -94,7 +95,10 @@ import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
|
94
95
|
import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
|
|
95
96
|
import { dirname, join } from "path";
|
|
96
97
|
import {
|
|
98
|
+
InvalidRestoredSessionError,
|
|
99
|
+
RuntimeDelegationRejectedError,
|
|
97
100
|
TinyCloudNode,
|
|
101
|
+
UnsupportedSessionRestoreError,
|
|
98
102
|
isCapabilitySubset,
|
|
99
103
|
principalDidEquals,
|
|
100
104
|
siweRuntimeDelegation
|
|
@@ -103,6 +107,7 @@ import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk
|
|
|
103
107
|
import { invokeOperation } from "@tinycloud/operations";
|
|
104
108
|
var PROFILE = "agent";
|
|
105
109
|
var MAX_CALLBACK_BYTES = 256 * 1024;
|
|
110
|
+
var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
|
|
106
111
|
var BOOTSTRAP_PERMISSIONS = [
|
|
107
112
|
{
|
|
108
113
|
service: "tinycloud.kv",
|
|
@@ -123,6 +128,26 @@ var ApprovalOwnerMismatchError = class extends Error {
|
|
|
123
128
|
this.name = "ApprovalOwnerMismatchError";
|
|
124
129
|
}
|
|
125
130
|
};
|
|
131
|
+
var ApprovalStageError = class extends Error {
|
|
132
|
+
stage;
|
|
133
|
+
code;
|
|
134
|
+
reason;
|
|
135
|
+
constructor(stage, code, reason, cause) {
|
|
136
|
+
super(`The approval failed at ${stage} (${code}).`, { cause });
|
|
137
|
+
this.name = "ApprovalStageError";
|
|
138
|
+
this.stage = stage;
|
|
139
|
+
this.code = code;
|
|
140
|
+
this.reason = reason;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
var ApprovalRequestError = class extends Error {
|
|
144
|
+
detail;
|
|
145
|
+
constructor(detail, message, cause) {
|
|
146
|
+
super(message, { cause });
|
|
147
|
+
this.name = "ApprovalRequestError";
|
|
148
|
+
this.detail = detail;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
126
151
|
var RemoteTenantStore = class {
|
|
127
152
|
#config;
|
|
128
153
|
#initializing = /* @__PURE__ */ new Map();
|
|
@@ -188,11 +213,20 @@ var RemoteTenantStore = class {
|
|
|
188
213
|
}
|
|
189
214
|
async completeApproval(state, request) {
|
|
190
215
|
const length = Number(request.headers.get("content-length") ?? "0");
|
|
191
|
-
if (Number.isFinite(length) && length > MAX_CALLBACK_BYTES)
|
|
216
|
+
if (Number.isFinite(length) && length > MAX_CALLBACK_BYTES) {
|
|
217
|
+
throw new ApprovalRequestError("body_invalid", "Callback body is too large.");
|
|
218
|
+
}
|
|
192
219
|
const text = await request.text();
|
|
193
|
-
if (Buffer.byteLength(text) > MAX_CALLBACK_BYTES)
|
|
194
|
-
|
|
195
|
-
|
|
220
|
+
if (Buffer.byteLength(text) > MAX_CALLBACK_BYTES) {
|
|
221
|
+
throw new ApprovalRequestError("body_invalid", "Callback body is too large.");
|
|
222
|
+
}
|
|
223
|
+
let data;
|
|
224
|
+
try {
|
|
225
|
+
data = JSON.parse(text);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
throw new ApprovalRequestError("body_invalid", "Callback body is invalid.", error);
|
|
228
|
+
}
|
|
229
|
+
if (!isRecord(data)) throw new ApprovalRequestError("body_invalid", "Callback body is invalid.");
|
|
196
230
|
const pending = await this.#readPending(state, true);
|
|
197
231
|
try {
|
|
198
232
|
const tenant = await this.#readTenant(pending.tenantId);
|
|
@@ -230,16 +264,26 @@ var RemoteTenantStore = class {
|
|
|
230
264
|
if (pending.requestId === void 0) throw new Error("Authority request is missing.");
|
|
231
265
|
const { node, session } = await this.#restoreApproval(tenant, data);
|
|
232
266
|
if (!pending.ownerDids.some((did) => samePrincipal(node.did, did))) throw new ApprovalOwnerMismatchError();
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
267
|
+
let delegation;
|
|
268
|
+
try {
|
|
269
|
+
delegation = siweRuntimeDelegation(node, {
|
|
270
|
+
authorization: session.delegationHeader.Authorization,
|
|
271
|
+
cid: session.delegationCid,
|
|
272
|
+
spaceId: session.spaceId,
|
|
273
|
+
ownerAddress: session.address,
|
|
274
|
+
chainId: session.chainId,
|
|
275
|
+
siwe: session.siwe,
|
|
276
|
+
signature: session.signature,
|
|
277
|
+
host: this.#config.nodeHost
|
|
278
|
+
});
|
|
279
|
+
} catch (error) {
|
|
280
|
+
throw new ApprovalStageError(
|
|
281
|
+
"siwe_runtime_delegation",
|
|
282
|
+
"DELEGATION_REJECTED",
|
|
283
|
+
error instanceof RuntimeDelegationRejectedError ? error.reason : void 0,
|
|
284
|
+
error
|
|
285
|
+
);
|
|
286
|
+
}
|
|
243
287
|
const result = await invokeOperation(
|
|
244
288
|
"tinycloud.auth.import",
|
|
245
289
|
1,
|
|
@@ -252,7 +296,15 @@ var RemoteTenantStore = class {
|
|
|
252
296
|
delegation
|
|
253
297
|
}
|
|
254
298
|
);
|
|
255
|
-
if (result.status
|
|
299
|
+
if (result.status === "error") {
|
|
300
|
+
const reason = result.error.details?.reason;
|
|
301
|
+
throw new ApprovalStageError(
|
|
302
|
+
result.error.code === "DELEGATION_REJECTED" && typeof reason === "string" ? "activation" : "auth_import",
|
|
303
|
+
result.error.code,
|
|
304
|
+
typeof reason === "string" ? reason : void 0
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
if (result.status !== "ok") throw new ApprovalStageError("auth_import", "AUTHORITY_REQUIRED");
|
|
256
308
|
}
|
|
257
309
|
/**
|
|
258
310
|
* Verify the OpenKey callback as a SIWE session for this tenant's key. The
|
|
@@ -262,11 +314,25 @@ var RemoteTenantStore = class {
|
|
|
262
314
|
* exactly the delegation being stored.
|
|
263
315
|
*/
|
|
264
316
|
async #restoreApproval(tenant, data) {
|
|
265
|
-
|
|
317
|
+
let session;
|
|
318
|
+
try {
|
|
319
|
+
session = sessionFromCallback(data, tenant.jwk);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
throw new ApprovalStageError("restore_approval", "CALLBACK_INCOMPLETE", void 0, error);
|
|
322
|
+
}
|
|
266
323
|
const node = new TinyCloudNode({ host: this.#config.nodeHost });
|
|
267
|
-
|
|
324
|
+
try {
|
|
325
|
+
await node.restoreSession(session);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
throw new ApprovalStageError(
|
|
328
|
+
"restore_approval",
|
|
329
|
+
error instanceof InvalidRestoredSessionError || error instanceof UnsupportedSessionRestoreError ? error.code : "SESSION_RESTORE_FAILED",
|
|
330
|
+
void 0,
|
|
331
|
+
error
|
|
332
|
+
);
|
|
333
|
+
}
|
|
268
334
|
if (!samePrincipal(node.sessionDid, tenant.sessionDid)) {
|
|
269
|
-
throw new
|
|
335
|
+
throw new ApprovalStageError("restore_approval", "SESSION_MISMATCH");
|
|
270
336
|
}
|
|
271
337
|
return { node, session };
|
|
272
338
|
}
|
|
@@ -307,7 +373,8 @@ var RemoteTenantStore = class {
|
|
|
307
373
|
sessionDid = didFromJwk(jwk);
|
|
308
374
|
}
|
|
309
375
|
const profilePath = this.#profilePath(stateRoot);
|
|
310
|
-
|
|
376
|
+
const profile = await readJson(profilePath);
|
|
377
|
+
if (profile === null) {
|
|
311
378
|
await secureWrite(profilePath, {
|
|
312
379
|
name: PROFILE,
|
|
313
380
|
host: this.#config.nodeHost,
|
|
@@ -321,14 +388,20 @@ var RemoteTenantStore = class {
|
|
|
321
388
|
authMethod: "openkey",
|
|
322
389
|
openkeyHost: this.#config.openkeyHost
|
|
323
390
|
});
|
|
391
|
+
} else if (typeof profile.host === "string" && profile.host.replace(/\/+$/, "") === LEGACY_DEFAULT_NODE_HOST && this.#config.nodeHost.replace(/\/+$/, "") !== LEGACY_DEFAULT_NODE_HOST) {
|
|
392
|
+
await secureWrite(profilePath, { ...profile, host: this.#config.nodeHost });
|
|
393
|
+
process.stderr.write(
|
|
394
|
+
`[tinycloud-mcp-http] tenant node host migrated: from=${LEGACY_DEFAULT_NODE_HOST} to=${new URL(this.#config.nodeHost).origin}
|
|
395
|
+
`
|
|
396
|
+
);
|
|
324
397
|
}
|
|
325
398
|
return { stateRoot, sessionDid, jwk };
|
|
326
399
|
}
|
|
327
400
|
async #readTenant(tenantId) {
|
|
328
|
-
if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new
|
|
401
|
+
if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
329
402
|
const stateRoot = join(this.#config.stateDir, "tenants", tenantId);
|
|
330
403
|
const jwk = await readJson(this.#keyPath(stateRoot));
|
|
331
|
-
if (jwk === null) throw new
|
|
404
|
+
if (jwk === null) throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.");
|
|
332
405
|
return { stateRoot, jwk, sessionDid: didFromJwk(jwk) };
|
|
333
406
|
}
|
|
334
407
|
async #createPending(subject, ownerDids, kind, permissions, requestId) {
|
|
@@ -358,26 +431,40 @@ var RemoteTenantStore = class {
|
|
|
358
431
|
async #readPending(state, consume) {
|
|
359
432
|
const [tenantId, nonce, signature] = state.split(".");
|
|
360
433
|
if (tenantId === void 0 || nonce === void 0 || signature === void 0) {
|
|
361
|
-
throw new
|
|
434
|
+
throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
362
435
|
}
|
|
363
436
|
const expected = this.#signState(tenantId, nonce);
|
|
364
437
|
const supplied = Buffer.from(signature, "base64url");
|
|
365
438
|
const expectedBytes = Buffer.from(expected, "base64url");
|
|
366
439
|
if (supplied.length !== expectedBytes.length || !timingSafeEqual(supplied, expectedBytes)) {
|
|
367
|
-
throw new
|
|
440
|
+
throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
368
441
|
}
|
|
369
442
|
const stateRoot = join(this.#config.stateDir, "tenants", tenantId);
|
|
370
443
|
const path = this.#pendingPath(stateRoot, nonce);
|
|
371
444
|
const claimed = `${path}.processing`;
|
|
372
|
-
if (consume)
|
|
445
|
+
if (consume) {
|
|
446
|
+
try {
|
|
447
|
+
await rename(path, claimed);
|
|
448
|
+
} catch (error) {
|
|
449
|
+
if (!isRecord(error) || error.code !== "ENOENT") throw error;
|
|
450
|
+
const tenantExists = await readJson(this.#keyPath(stateRoot)).then(
|
|
451
|
+
(jwk) => jwk !== null,
|
|
452
|
+
() => true
|
|
453
|
+
);
|
|
454
|
+
if (!tenantExists) {
|
|
455
|
+
throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.", error);
|
|
456
|
+
}
|
|
457
|
+
throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.", error);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
373
460
|
try {
|
|
374
461
|
const pending = await readJson(consume ? claimed : path);
|
|
375
462
|
if (pending === null || pending.version !== 1 || pending.tenantId !== tenantId || pending.nonce !== nonce) {
|
|
376
|
-
throw new
|
|
463
|
+
throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
|
|
377
464
|
}
|
|
378
465
|
if (new Date(pending.expiresAt).getTime() <= Date.now()) {
|
|
379
466
|
await rm(consume ? claimed : path, { force: true });
|
|
380
|
-
throw new
|
|
467
|
+
throw new ApprovalRequestError("state_expired", "Approval state has expired.");
|
|
381
468
|
}
|
|
382
469
|
return pending;
|
|
383
470
|
} catch (error) {
|
|
@@ -500,9 +587,6 @@ async function readJson(path) {
|
|
|
500
587
|
throw error;
|
|
501
588
|
}
|
|
502
589
|
}
|
|
503
|
-
async function fileExists(path) {
|
|
504
|
-
return await readJson(path) !== null;
|
|
505
|
-
}
|
|
506
590
|
async function reusablePending(directory, expected) {
|
|
507
591
|
let names;
|
|
508
592
|
try {
|
|
@@ -824,7 +908,7 @@ function registerTinyCloudTools(server, selection, validator) {
|
|
|
824
908
|
// package.json
|
|
825
909
|
var package_default = {
|
|
826
910
|
name: "@tinycloud/mcp",
|
|
827
|
-
version: "0.5.4-beta.
|
|
911
|
+
version: "0.5.4-beta.2",
|
|
828
912
|
description: "TinyCloud delegated operations MCP server",
|
|
829
913
|
keywords: [
|
|
830
914
|
"tinycloud",
|
|
@@ -897,7 +981,7 @@ var package_default = {
|
|
|
897
981
|
"@modelcontextprotocol/server": "2.0.0-beta.4",
|
|
898
982
|
"@tinycloud/node-sdk": "3.2.4-beta.0",
|
|
899
983
|
"@tinycloud/node-sdk-wasm": "1.7.7",
|
|
900
|
-
"@tinycloud/operations": "0.5.4-beta.
|
|
984
|
+
"@tinycloud/operations": "0.5.4-beta.1",
|
|
901
985
|
jose: "6.1.3"
|
|
902
986
|
},
|
|
903
987
|
devDependencies: {
|
|
@@ -1000,8 +1084,23 @@ function createHostedMcpApp(config) {
|
|
|
1000
1084
|
await store.completeApproval(requiredState(url), request);
|
|
1001
1085
|
return callbackCors(json({ success: true }), config.openkeyHost);
|
|
1002
1086
|
} catch (error) {
|
|
1087
|
+
if (error instanceof ApprovalOwnerMismatchError) {
|
|
1088
|
+
return callbackCors(json({ error: error.message, code: "approval_owner_mismatch" }, 403), config.openkeyHost);
|
|
1089
|
+
}
|
|
1090
|
+
const stage = error instanceof ApprovalStageError ? error.stage : "unknown";
|
|
1091
|
+
const code = error instanceof ApprovalStageError && /^[A-Z][A-Z0-9_]{0,63}$/.test(error.code) ? error.code : "UNKNOWN";
|
|
1092
|
+
const reason = !(error instanceof ApprovalStageError) || error.reason === void 0 ? void 0 : RUNTIME_DELEGATION_REJECTIONS.find((known) => known === error.reason) ?? "unknown";
|
|
1093
|
+
const detail = error instanceof ApprovalRequestError ? error.detail : "none";
|
|
1094
|
+
writeDiagnostic(
|
|
1095
|
+
`connect callback rejected: stage=${stage} code=${code}${reason === void 0 ? "" : ` reason=${reason}`} detail=${detail}`
|
|
1096
|
+
);
|
|
1003
1097
|
return callbackCors(
|
|
1004
|
-
|
|
1098
|
+
json({
|
|
1099
|
+
error: stage,
|
|
1100
|
+
code,
|
|
1101
|
+
...reason === void 0 ? {} : { reason },
|
|
1102
|
+
message: "TinyCloud could not accept this delegation."
|
|
1103
|
+
}, 400),
|
|
1005
1104
|
config.openkeyHost
|
|
1006
1105
|
);
|
|
1007
1106
|
}
|
|
@@ -1076,7 +1175,8 @@ function canonicalResourceUrl(url) {
|
|
|
1076
1175
|
}
|
|
1077
1176
|
function requiredState(url) {
|
|
1078
1177
|
const state = url.searchParams.get("state");
|
|
1079
|
-
if (state === null
|
|
1178
|
+
if (state === null) throw new ApprovalRequestError("state_missing", "Missing approval state.");
|
|
1179
|
+
if (state.length > 512) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
1080
1180
|
return state;
|
|
1081
1181
|
}
|
|
1082
1182
|
function validateOrigin(request, allowed) {
|