@tinycloud/mcp 0.8.3-beta.15 → 0.8.3-beta.17
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 +17 -42
- 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 +763 -18
- package/dist/http-cli.cjs.map +1 -1
- package/dist/http-cli.js +762 -14
- package/dist/http-cli.js.map +1 -1
- package/dist/http.cjs +762 -18
- package/dist/http.cjs.map +1 -1
- package/dist/http.d.cts +2 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.js +761 -14
- package/dist/http.js.map +1 -1
- package/dist/index.cjs +762 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +761 -14
- 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/dist/tools.cjs +1 -1
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.d.cts +3 -1
- package/dist/tools.d.ts +3 -1
- package/dist/tools.js +1 -1
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
- package/skills/tinycloud-delegated-secrets/SKILL.md +17 -3
package/dist/http.js
CHANGED
|
@@ -259,12 +259,15 @@ function logToolCall(tool, code, startedAt, replayedGrants) {
|
|
|
259
259
|
}
|
|
260
260
|
|
|
261
261
|
// src/remote-store.ts
|
|
262
|
-
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
262
|
+
import { createHmac, randomBytes, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
263
263
|
import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
|
|
264
264
|
import { dirname, join } from "path";
|
|
265
265
|
import {
|
|
266
|
+
activateValidatedRuntimeDelegation,
|
|
267
|
+
deserializeDelegation,
|
|
266
268
|
InvalidRestoredSessionError,
|
|
267
269
|
RuntimeDelegationRejectedError,
|
|
270
|
+
serializeDelegation,
|
|
268
271
|
TinyCloudNode,
|
|
269
272
|
UnsupportedSessionRestoreError,
|
|
270
273
|
isCapabilitySubset,
|
|
@@ -273,11 +276,80 @@ import {
|
|
|
273
276
|
signedCompactUcanExpiry
|
|
274
277
|
} from "@tinycloud/node-sdk";
|
|
275
278
|
import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk-wasm";
|
|
279
|
+
import { createOrReusePermissionRequest } from "@tinycloud/operations/artifacts";
|
|
280
|
+
import { withTinyCloudStateRoot } from "@tinycloud/operations/state";
|
|
276
281
|
import { invokeOperation } from "@tinycloud/operations";
|
|
277
282
|
import { nodeRevocationOf, operationRetryDescriptor } from "@tinycloud/operations/mcp-authority";
|
|
278
283
|
import { tcAppsApprovalMessage, tcAppsApprovalReason } from "@tinycloud/operations/tc-apps";
|
|
284
|
+
|
|
285
|
+
// src/native-auth.ts
|
|
286
|
+
import { timingSafeEqual } from "crypto";
|
|
287
|
+
var NATIVE_SERVICE_CREDENTIAL_HEADER = "X-TinyCloud-Service-Credential";
|
|
288
|
+
var NATIVE_OWNER_DID_HEADER = "X-TinyCloud-Owner-DID";
|
|
289
|
+
var NATIVE_GRANT_MAX_LIFETIME_SECONDS = 30 * 24 * 60 * 60;
|
|
290
|
+
var NATIVE_GRANT_REQUIRED = "native_grant_required";
|
|
291
|
+
var NATIVE_GRANT_EXPIRED = "native_grant_expired";
|
|
292
|
+
var NATIVE_GRANT_REJECTED = "native_grant_rejected";
|
|
293
|
+
var NATIVE_SERVICE_UNAUTHORIZED = "native_service_unauthorized";
|
|
294
|
+
var NATIVE_SERVICE_UNAVAILABLE = "native_service_unavailable";
|
|
295
|
+
var NATIVE_GRANT_VALIDATION_UNAVAILABLE = "native_grant_validation_unavailable";
|
|
296
|
+
var MAX_NATIVE_GRANT_BODY_BYTES = 256 * 1024;
|
|
297
|
+
function serviceCredentialMatches(supplied, configured) {
|
|
298
|
+
if (supplied === null || configured === void 0) return false;
|
|
299
|
+
const suppliedBytes = Buffer.from(supplied, "utf8");
|
|
300
|
+
const configuredBytes = Buffer.from(configured, "utf8");
|
|
301
|
+
return suppliedBytes.byteLength === configuredBytes.byteLength && timingSafeEqual(suppliedBytes, configuredBytes);
|
|
302
|
+
}
|
|
303
|
+
var OWNER_APPLICATIONS_SPACE = /^tinycloud:pkh:eip155:([1-9][0-9]*):(0x[0-9a-f]{40}):applications$/i;
|
|
304
|
+
var SQL_PATH = "xyz.tinycloud.tinychat/connectors";
|
|
305
|
+
var KV_PREFIX = "xyz.tinycloud.tinychat/connectors/";
|
|
306
|
+
function canonicalOwnerDid(value) {
|
|
307
|
+
const match = /^did:pkh:eip155:([1-9][0-9]*):(0x[0-9a-f]{40})$/i.exec(value);
|
|
308
|
+
if (!match) return void 0;
|
|
309
|
+
const chainId = Number(match[1]);
|
|
310
|
+
if (!Number.isSafeInteger(chainId)) return void 0;
|
|
311
|
+
return `did:pkh:eip155:${chainId}:${match[2].toLowerCase()}`;
|
|
312
|
+
}
|
|
313
|
+
function nativeOwnerForConnectorGrant(permissions) {
|
|
314
|
+
if (permissions.length !== 2) return void 0;
|
|
315
|
+
const sqlEntries = permissions.filter((permission) => permission.service === "tinycloud.sql");
|
|
316
|
+
const kvEntries = permissions.filter((permission) => permission.service === "tinycloud.kv");
|
|
317
|
+
if (sqlEntries.length !== 1 || kvEntries.length !== 1) return void 0;
|
|
318
|
+
const sql = sqlEntries[0];
|
|
319
|
+
const kv = kvEntries[0];
|
|
320
|
+
if (typeof sql.space !== "string" || typeof kv.space !== "string" || sql.path !== SQL_PATH || kv.path !== KV_PREFIX) return void 0;
|
|
321
|
+
if (!actionsEqual(sql.actions, ["tinycloud.sql/read"]) || !actionsEqual(kv.actions, ["tinycloud.kv/get", "tinycloud.kv/list"])) return void 0;
|
|
322
|
+
if (hasCaveats(sql) || hasCaveats(kv)) return void 0;
|
|
323
|
+
const sqlSpace = ownerFromApplicationsSpace(sql.space);
|
|
324
|
+
const kvSpace = ownerFromApplicationsSpace(kv.space);
|
|
325
|
+
return sqlSpace !== void 0 && sqlSpace === kvSpace ? sqlSpace : void 0;
|
|
326
|
+
}
|
|
327
|
+
function ownerFromApplicationsSpace(space) {
|
|
328
|
+
const match = OWNER_APPLICATIONS_SPACE.exec(space);
|
|
329
|
+
if (!match) return void 0;
|
|
330
|
+
const chainId = Number(match[1]);
|
|
331
|
+
if (!Number.isSafeInteger(chainId)) return void 0;
|
|
332
|
+
return `did:pkh:eip155:${chainId}:${match[2].toLowerCase()}`;
|
|
333
|
+
}
|
|
334
|
+
function actionsEqual(actual, expected) {
|
|
335
|
+
const actualActions = [...actual].sort();
|
|
336
|
+
const expectedActions = [...expected].sort();
|
|
337
|
+
return actualActions.length === expectedActions.length && actualActions.every((value, index) => value === expectedActions[index]);
|
|
338
|
+
}
|
|
339
|
+
function hasCaveats(permission) {
|
|
340
|
+
return Object.hasOwn(permission, "caveats") && permission.caveats !== void 0;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// src/remote-store.ts
|
|
279
344
|
var PROFILE = "agent";
|
|
280
345
|
var MAX_CALLBACK_BYTES = 256 * 1024;
|
|
346
|
+
var NATIVE_GRANT_IMPORT_REJECTION_CODES = /* @__PURE__ */ new Set([
|
|
347
|
+
"DELEGATION_ARTIFACT_INVALID",
|
|
348
|
+
"DELEGATION_EXPIRED",
|
|
349
|
+
"DELEGATION_AUDIENCE_MISMATCH",
|
|
350
|
+
"DELEGATION_HOST_MISMATCH",
|
|
351
|
+
"DELEGATION_REJECTED"
|
|
352
|
+
]);
|
|
281
353
|
var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
|
|
282
354
|
var BOOTSTRAP_PERMISSIONS = [
|
|
283
355
|
{
|
|
@@ -319,6 +391,15 @@ var ApprovalRequestError = class extends Error {
|
|
|
319
391
|
this.detail = detail;
|
|
320
392
|
}
|
|
321
393
|
};
|
|
394
|
+
var NativeGrantValidationError = class extends Error {
|
|
395
|
+
code;
|
|
396
|
+
constructor(status) {
|
|
397
|
+
const code = status.startsWith("native_") ? status : status === "required" ? "native_grant_required" : status === "expired" ? "native_grant_expired" : status === "unavailable" ? "native_grant_validation_unavailable" : "native_grant_rejected";
|
|
398
|
+
super(code);
|
|
399
|
+
this.name = "NativeGrantValidationError";
|
|
400
|
+
this.code = code;
|
|
401
|
+
}
|
|
402
|
+
};
|
|
322
403
|
var RemoteTenantStore = class {
|
|
323
404
|
#config;
|
|
324
405
|
#initializing = /* @__PURE__ */ new Map();
|
|
@@ -331,9 +412,186 @@ var RemoteTenantStore = class {
|
|
|
331
412
|
#scopedClearing = /* @__PURE__ */ new Map();
|
|
332
413
|
#clearedScoped = /* @__PURE__ */ new Map();
|
|
333
414
|
#approvalProcessing = /* @__PURE__ */ new Map();
|
|
415
|
+
#nativeIdentity;
|
|
416
|
+
#nativeGrantQueues = /* @__PURE__ */ new Map();
|
|
334
417
|
constructor(config) {
|
|
335
418
|
this.#config = config;
|
|
336
419
|
}
|
|
420
|
+
async nativeServiceIdentity() {
|
|
421
|
+
const identity = await this.#loadNativeServiceKey();
|
|
422
|
+
return { serviceDid: normalizeDid(identity.did) };
|
|
423
|
+
}
|
|
424
|
+
nativeTenantStateRoot(ownerDid) {
|
|
425
|
+
const canonical = canonicalOwnerDid(ownerDid);
|
|
426
|
+
if (canonical === void 0) throw new NativeGrantValidationError("rejected");
|
|
427
|
+
const tenant = createHmac("sha256", this.#config.stateSecret).update(`native-tenant:${canonical}`).digest("hex");
|
|
428
|
+
return join(this.#config.stateDir, "native-tenants", tenant);
|
|
429
|
+
}
|
|
430
|
+
async acceptNativeGrant(serializedDelegation) {
|
|
431
|
+
if (Buffer.byteLength(serializedDelegation) > MAX_CALLBACK_BYTES) {
|
|
432
|
+
throw new NativeGrantValidationError("rejected");
|
|
433
|
+
}
|
|
434
|
+
const identity = await this.#loadNativeServiceKey();
|
|
435
|
+
const activated = await this.#activateNativeDelegation(serializedDelegation, identity);
|
|
436
|
+
const { ownerDid, spaceId, permissions, expiresAt } = activated;
|
|
437
|
+
return this.#withNativeGrantLock(ownerDid, async () => {
|
|
438
|
+
const stateRoot = this.nativeTenantStateRoot(ownerDid);
|
|
439
|
+
let previous = null;
|
|
440
|
+
try {
|
|
441
|
+
const stored = await readJson(this.#nativeGrantPath(stateRoot));
|
|
442
|
+
if (isRecord(stored) && stored.version === 1 && stored.ownerDid === ownerDid && typeof stored.serviceDid === "string" && samePrincipal(stored.serviceDid, identity.did) && typeof stored.serializedDelegation === "string" && typeof stored.delegationCid === "string" && typeof stored.spaceId === "string" && Array.isArray(stored.permissions) && stored.permissions.every(isPermissionEntry) && nativeOwnerForConnectorGrant(stored.permissions) === ownerDid && typeof stored.expiresAt === "string" && Number.isFinite(Date.parse(stored.expiresAt))) {
|
|
443
|
+
previous = stored;
|
|
444
|
+
}
|
|
445
|
+
} catch (error) {
|
|
446
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
447
|
+
}
|
|
448
|
+
if (previous !== null) {
|
|
449
|
+
const previousExpiry = Date.parse(previous.expiresAt);
|
|
450
|
+
const nextExpiry = expiresAt.getTime();
|
|
451
|
+
if (previousExpiry > nextExpiry || previousExpiry === nextExpiry && previous.delegationCid !== activated.cid) {
|
|
452
|
+
throw new NativeGrantValidationError("rejected");
|
|
453
|
+
}
|
|
454
|
+
if (previous.delegationCid === activated.cid) {
|
|
455
|
+
return {
|
|
456
|
+
ownerDid,
|
|
457
|
+
serviceDid: normalizeDid(identity.did),
|
|
458
|
+
expiresAt: previous.expiresAt,
|
|
459
|
+
stateRoot
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const session = {
|
|
464
|
+
delegationHeader: activated.delegation.delegationHeader,
|
|
465
|
+
delegationCid: activated.cid,
|
|
466
|
+
spaceId,
|
|
467
|
+
jwk: identity.jwk,
|
|
468
|
+
verificationMethod: identity.did,
|
|
469
|
+
tinycloudHosts: [this.#config.nodeHost]
|
|
470
|
+
};
|
|
471
|
+
const grant = {
|
|
472
|
+
version: 1,
|
|
473
|
+
ownerDid,
|
|
474
|
+
serviceDid: normalizeDid(identity.did),
|
|
475
|
+
serializedDelegation: serializeDelegation(activated.delegation),
|
|
476
|
+
delegationCid: activated.cid,
|
|
477
|
+
spaceId,
|
|
478
|
+
permissions,
|
|
479
|
+
expiresAt: expiresAt.toISOString()
|
|
480
|
+
};
|
|
481
|
+
const sessionPath = this.#sessionPath(stateRoot);
|
|
482
|
+
const previousSession = await readOptionalText(sessionPath);
|
|
483
|
+
try {
|
|
484
|
+
await secureWrite(this.#profilePath(stateRoot), {
|
|
485
|
+
name: PROFILE,
|
|
486
|
+
host: this.#config.nodeHost,
|
|
487
|
+
chainId: activated.delegation.chainId,
|
|
488
|
+
spaceName: "applications",
|
|
489
|
+
did: identity.did,
|
|
490
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
491
|
+
profile: PROFILE,
|
|
492
|
+
operatorType: "agent",
|
|
493
|
+
posture: "delegate-session",
|
|
494
|
+
ownerDid,
|
|
495
|
+
sessionDid: identity.did,
|
|
496
|
+
spaceId,
|
|
497
|
+
tinycloudHost: this.#config.nodeHost
|
|
498
|
+
});
|
|
499
|
+
await secureWrite(this.#keyPath(stateRoot), identity.jwk);
|
|
500
|
+
await secureWrite(sessionPath, session);
|
|
501
|
+
const request = await withTinyCloudStateRoot(stateRoot, () => createOrReusePermissionRequest({
|
|
502
|
+
profile: PROFILE,
|
|
503
|
+
posture: "delegate-session",
|
|
504
|
+
operatorType: "agent",
|
|
505
|
+
host: this.#config.nodeHost,
|
|
506
|
+
sessionDid: identity.did,
|
|
507
|
+
ownerDid,
|
|
508
|
+
spaceId,
|
|
509
|
+
missing: permissions,
|
|
510
|
+
granted: [],
|
|
511
|
+
replace: true
|
|
512
|
+
}));
|
|
513
|
+
if (request.status !== "created") {
|
|
514
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
515
|
+
}
|
|
516
|
+
const imported = await invokeOperation(
|
|
517
|
+
"tinycloud.auth.import",
|
|
518
|
+
1,
|
|
519
|
+
{ profile: PROFILE, stateRoot },
|
|
520
|
+
{
|
|
521
|
+
kind: "tinycloud.auth.delegation",
|
|
522
|
+
version: 1,
|
|
523
|
+
requestId: request.request.requestId,
|
|
524
|
+
delegationCid: activated.cid,
|
|
525
|
+
delegation: activated.delegation
|
|
526
|
+
}
|
|
527
|
+
);
|
|
528
|
+
if (imported.status === "error") {
|
|
529
|
+
throw new NativeGrantValidationError(
|
|
530
|
+
NATIVE_GRANT_IMPORT_REJECTION_CODES.has(imported.error.code) ? "native_grant_rejected" : "native_grant_validation_unavailable"
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
if (imported.status !== "ok" || !isRecord(imported.output) || imported.output.activated !== true || imported.output.cid !== activated.cid) {
|
|
534
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
535
|
+
}
|
|
536
|
+
await secureWrite(this.#nativeGrantPath(stateRoot), grant);
|
|
537
|
+
} catch (error) {
|
|
538
|
+
try {
|
|
539
|
+
await restoreOptionalText(sessionPath, previousSession);
|
|
540
|
+
} catch {
|
|
541
|
+
await rm(this.#nativeGrantPath(stateRoot), { force: true }).catch(() => void 0);
|
|
542
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
543
|
+
}
|
|
544
|
+
throw error;
|
|
545
|
+
}
|
|
546
|
+
return { ownerDid, serviceDid: normalizeDid(identity.did), expiresAt: grant.expiresAt, stateRoot };
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
async nativeGrantForOwner(ownerDid) {
|
|
550
|
+
const canonical = canonicalOwnerDid(ownerDid);
|
|
551
|
+
if (canonical === void 0) return { status: "rejected" };
|
|
552
|
+
const stateRoot = this.nativeTenantStateRoot(canonical);
|
|
553
|
+
let stored;
|
|
554
|
+
try {
|
|
555
|
+
stored = await readJson(this.#nativeGrantPath(stateRoot));
|
|
556
|
+
} catch {
|
|
557
|
+
return { status: "rejected" };
|
|
558
|
+
}
|
|
559
|
+
if (stored === null) return { status: "required" };
|
|
560
|
+
if (!isRecord(stored) || stored.version !== 1 || stored.ownerDid !== canonical || typeof stored.serviceDid !== "string" || typeof stored.serializedDelegation !== "string" || typeof stored.delegationCid !== "string" || typeof stored.spaceId !== "string" || typeof stored.expiresAt !== "string" || !Array.isArray(stored.permissions) || !stored.permissions.every(isPermissionEntry) || nativeOwnerForConnectorGrant(stored.permissions) !== canonical) {
|
|
561
|
+
return { status: "rejected" };
|
|
562
|
+
}
|
|
563
|
+
const expires = Date.parse(stored.expiresAt);
|
|
564
|
+
if (!Number.isFinite(expires)) return { status: "rejected" };
|
|
565
|
+
if (expires <= Date.now()) return { status: "expired" };
|
|
566
|
+
try {
|
|
567
|
+
const identity = await this.#loadNativeServiceKey();
|
|
568
|
+
if (!samePrincipal(stored.serviceDid, identity.did)) return { status: "rejected" };
|
|
569
|
+
const activated = await this.#activateNativeDelegation(stored.serializedDelegation, identity);
|
|
570
|
+
if (activated.ownerDid !== canonical || activated.cid !== stored.delegationCid || activated.spaceId !== stored.spaceId || JSON.stringify(activated.permissions) !== JSON.stringify(stored.permissions) || activated.expiresAt.toISOString() !== stored.expiresAt) return { status: "rejected" };
|
|
571
|
+
return { status: "ok", ownerDid: canonical, stateRoot, expiresAt: stored.expiresAt };
|
|
572
|
+
} catch (error) {
|
|
573
|
+
if (error instanceof NativeGrantValidationError) {
|
|
574
|
+
return { status: error.code === "native_grant_expired" ? "expired" : error.code === "native_grant_validation_unavailable" ? "unavailable" : "rejected" };
|
|
575
|
+
}
|
|
576
|
+
return { status: "unavailable" };
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
async assertNativeGrantActive(ownerDid) {
|
|
580
|
+
const canonical = canonicalOwnerDid(ownerDid);
|
|
581
|
+
if (canonical === void 0) throw new NativeGrantValidationError("rejected");
|
|
582
|
+
const stateRoot = this.nativeTenantStateRoot(canonical);
|
|
583
|
+
let stored;
|
|
584
|
+
try {
|
|
585
|
+
stored = await readJson(this.#nativeGrantPath(stateRoot));
|
|
586
|
+
} catch {
|
|
587
|
+
throw new NativeGrantValidationError("rejected");
|
|
588
|
+
}
|
|
589
|
+
if (stored === null) throw new NativeGrantValidationError("required");
|
|
590
|
+
if (!isRecord(stored) || stored.version !== 1 || stored.ownerDid !== canonical || typeof stored.expiresAt !== "string" || !Number.isFinite(Date.parse(stored.expiresAt))) {
|
|
591
|
+
throw new NativeGrantValidationError("rejected");
|
|
592
|
+
}
|
|
593
|
+
if (Date.parse(stored.expiresAt) <= Date.now()) throw new NativeGrantValidationError("expired");
|
|
594
|
+
}
|
|
337
595
|
tenantStateRoot(subject) {
|
|
338
596
|
return join(this.#config.stateDir, "tenants", this.#tenantId(subject));
|
|
339
597
|
}
|
|
@@ -885,7 +1143,7 @@ var RemoteTenantStore = class {
|
|
|
885
1143
|
const expected = this.#signState(tenantId, nonce);
|
|
886
1144
|
const supplied = Buffer.from(signature, "base64url");
|
|
887
1145
|
const expectedBytes = Buffer.from(expected, "base64url");
|
|
888
|
-
if (supplied.length !== expectedBytes.length || !
|
|
1146
|
+
if (supplied.length !== expectedBytes.length || !timingSafeEqual2(supplied, expectedBytes)) {
|
|
889
1147
|
throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
890
1148
|
}
|
|
891
1149
|
const stateRoot = join(this.#config.stateDir, "tenants", tenantId);
|
|
@@ -936,6 +1194,123 @@ var RemoteTenantStore = class {
|
|
|
936
1194
|
url.searchParams.set("state", `${pending.tenantId}.${pending.nonce}.${this.#signState(pending.tenantId, pending.nonce)}`);
|
|
937
1195
|
return url.toString();
|
|
938
1196
|
}
|
|
1197
|
+
async #withNativeGrantLock(ownerDid, action) {
|
|
1198
|
+
const previous = this.#nativeGrantQueues.get(ownerDid) ?? Promise.resolve();
|
|
1199
|
+
let releaseNext;
|
|
1200
|
+
const next = new Promise((resolve) => {
|
|
1201
|
+
releaseNext = resolve;
|
|
1202
|
+
});
|
|
1203
|
+
this.#nativeGrantQueues.set(ownerDid, next);
|
|
1204
|
+
await previous;
|
|
1205
|
+
try {
|
|
1206
|
+
return await action();
|
|
1207
|
+
} finally {
|
|
1208
|
+
releaseNext();
|
|
1209
|
+
if (this.#nativeGrantQueues.get(ownerDid) === next) this.#nativeGrantQueues.delete(ownerDid);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
async #loadNativeServiceKey() {
|
|
1213
|
+
if (this.#nativeIdentity === void 0) {
|
|
1214
|
+
const loading = this.#readOrCreateNativeServiceKey();
|
|
1215
|
+
this.#nativeIdentity = loading;
|
|
1216
|
+
void loading.catch(() => {
|
|
1217
|
+
if (this.#nativeIdentity === loading) this.#nativeIdentity = void 0;
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
return this.#nativeIdentity;
|
|
1221
|
+
}
|
|
1222
|
+
async #readOrCreateNativeServiceKey() {
|
|
1223
|
+
const path = join(this.#config.stateDir, ".tinycloud", "native-service-key.json");
|
|
1224
|
+
await secureDirectory(dirname(path));
|
|
1225
|
+
let stored = await readJson(path);
|
|
1226
|
+
if (stored === null) {
|
|
1227
|
+
const generated = generateSessionKey();
|
|
1228
|
+
try {
|
|
1229
|
+
await writeFile(path, `${JSON.stringify({ version: 1, jwk: generated.jwk })}
|
|
1230
|
+
`, {
|
|
1231
|
+
mode: 384,
|
|
1232
|
+
flag: "wx"
|
|
1233
|
+
});
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
if (!isRecord(error) || error.code !== "EEXIST") throw error;
|
|
1236
|
+
}
|
|
1237
|
+
await chmod(path, 384);
|
|
1238
|
+
stored = await readJson(path);
|
|
1239
|
+
}
|
|
1240
|
+
if (stored === null || !isRecord(stored) || stored.version !== 1 || !isRecord(stored.jwk)) {
|
|
1241
|
+
throw new Error("The native MCP service identity is unavailable.");
|
|
1242
|
+
}
|
|
1243
|
+
return { did: didFromJwk(stored.jwk), jwk: stored.jwk };
|
|
1244
|
+
}
|
|
1245
|
+
async #activateNativeDelegation(serialized, identity) {
|
|
1246
|
+
let delegation;
|
|
1247
|
+
try {
|
|
1248
|
+
const parsed = JSON.parse(serialized);
|
|
1249
|
+
if (!isRecord(parsed)) throw new Error("invalid delegation");
|
|
1250
|
+
delegation = deserializeDelegation(serialized);
|
|
1251
|
+
} catch {
|
|
1252
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1253
|
+
}
|
|
1254
|
+
if (!isRecord(delegation.delegationHeader) || typeof delegation.delegationHeader.Authorization !== "string" || delegation.delegationHeader.Authorization.length === 0 || typeof delegation.cid !== "string" || delegation.cid.length === 0 || typeof delegation.delegateDID !== "string" || !samePrincipal(delegation.delegateDID, normalizeDid(identity.did)) || typeof delegation.spaceId !== "string" || typeof delegation.ownerAddress !== "string" || !Number.isSafeInteger(delegation.chainId) || typeof delegation.host !== "string" || !sameNodeHost(delegation.host, this.#config.nodeHost) || !(delegation.expiry instanceof Date) || !Number.isFinite(delegation.expiry.getTime())) {
|
|
1255
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1256
|
+
}
|
|
1257
|
+
const now = Date.now();
|
|
1258
|
+
const claimedExpiry = delegation.expiry.getTime();
|
|
1259
|
+
if (claimedExpiry <= now) throw new NativeGrantValidationError("native_grant_expired");
|
|
1260
|
+
if (claimedExpiry > now + NATIVE_GRANT_MAX_LIFETIME_SECONDS * 1e3) {
|
|
1261
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1262
|
+
}
|
|
1263
|
+
let ownerDid;
|
|
1264
|
+
let spaceId;
|
|
1265
|
+
const node = new TinyCloudNode({ host: this.#config.nodeHost });
|
|
1266
|
+
try {
|
|
1267
|
+
await node.restoreSession({
|
|
1268
|
+
delegationHeader: delegation.delegationHeader,
|
|
1269
|
+
delegationCid: delegation.cid,
|
|
1270
|
+
spaceId: delegation.spaceId,
|
|
1271
|
+
jwk: identity.jwk,
|
|
1272
|
+
verificationMethod: identity.did,
|
|
1273
|
+
tinycloudHosts: [this.#config.nodeHost]
|
|
1274
|
+
});
|
|
1275
|
+
const activated = await activateValidatedRuntimeDelegation(node, delegation, {
|
|
1276
|
+
host: this.#config.nodeHost,
|
|
1277
|
+
authorize: (permissions) => {
|
|
1278
|
+
const derivedOwner = nativeOwnerForConnectorGrant(permissions);
|
|
1279
|
+
const signedSpace = permissions[0]?.space;
|
|
1280
|
+
if (derivedOwner === void 0 || signedSpace === void 0) return false;
|
|
1281
|
+
const metadataOwner = canonicalOwnerDid(
|
|
1282
|
+
`did:pkh:eip155:${delegation.chainId}:${delegation.ownerAddress}`
|
|
1283
|
+
);
|
|
1284
|
+
if (metadataOwner !== derivedOwner || delegation.spaceId.toLowerCase() !== signedSpace.toLowerCase()) return false;
|
|
1285
|
+
ownerDid = derivedOwner;
|
|
1286
|
+
spaceId = signedSpace;
|
|
1287
|
+
return true;
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
if (ownerDid === void 0 || spaceId === void 0 || !samePrincipal(activated.audience, normalizeDid(identity.did)) || activated.expiry.getTime() > now + NATIVE_GRANT_MAX_LIFETIME_SECONDS * 1e3) {
|
|
1291
|
+
throw new NativeGrantValidationError("native_grant_rejected");
|
|
1292
|
+
}
|
|
1293
|
+
return {
|
|
1294
|
+
ownerDid,
|
|
1295
|
+
spaceId,
|
|
1296
|
+
permissions: [...activated.effectivePermissions],
|
|
1297
|
+
cid: activated.cid,
|
|
1298
|
+
expiresAt: activated.expiry,
|
|
1299
|
+
delegation: activated.delegation
|
|
1300
|
+
};
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
if (error instanceof NativeGrantValidationError) throw error;
|
|
1303
|
+
if (error instanceof RuntimeDelegationRejectedError) {
|
|
1304
|
+
throw new NativeGrantValidationError(
|
|
1305
|
+
error.reason === "expired" ? "native_grant_expired" : error.reason === "activation_unavailable" ? "native_grant_validation_unavailable" : "native_grant_rejected"
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
throw new NativeGrantValidationError("native_grant_validation_unavailable");
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
#nativeGrantPath(stateRoot) {
|
|
1312
|
+
return join(stateRoot, ".tinycloud", "native-grant.json");
|
|
1313
|
+
}
|
|
939
1314
|
#tenantId(subject) {
|
|
940
1315
|
return createHmac("sha256", this.#config.stateSecret).update(`tenant:${subject}`).digest("hex");
|
|
941
1316
|
}
|
|
@@ -1029,6 +1404,21 @@ function samePrincipal(left, right) {
|
|
|
1029
1404
|
function normalizeDid(value) {
|
|
1030
1405
|
return value.split("#", 1)[0];
|
|
1031
1406
|
}
|
|
1407
|
+
function sameNodeHost(left, right) {
|
|
1408
|
+
try {
|
|
1409
|
+
const normalize = (value) => {
|
|
1410
|
+
const url = new URL(value);
|
|
1411
|
+
if (url.username !== "" || url.password !== "") return "";
|
|
1412
|
+
url.hash = "";
|
|
1413
|
+
url.search = "";
|
|
1414
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
1415
|
+
return url.toString().replace(/\/+$/, "");
|
|
1416
|
+
};
|
|
1417
|
+
return normalize(left) !== "" && normalize(left) === normalize(right);
|
|
1418
|
+
} catch {
|
|
1419
|
+
return false;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1032
1422
|
function isPermissionEntry(value) {
|
|
1033
1423
|
return isRecord(value) && typeof value.service === "string" && typeof value.space === "string" && typeof value.path === "string" && Array.isArray(value.actions) && value.actions.every((action) => typeof action === "string");
|
|
1034
1424
|
}
|
|
@@ -1059,6 +1449,23 @@ async function readJson(path) {
|
|
|
1059
1449
|
throw error;
|
|
1060
1450
|
}
|
|
1061
1451
|
}
|
|
1452
|
+
async function readOptionalText(path) {
|
|
1453
|
+
try {
|
|
1454
|
+
return await readFile(path, "utf8");
|
|
1455
|
+
} catch (error) {
|
|
1456
|
+
if (isRecord(error) && error.code === "ENOENT") return null;
|
|
1457
|
+
throw error;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
async function restoreOptionalText(path, contents) {
|
|
1461
|
+
if (contents === null) {
|
|
1462
|
+
await rm(path, { force: true });
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
await secureDirectory(dirname(path));
|
|
1466
|
+
await writeFile(path, contents, { mode: 384 });
|
|
1467
|
+
await chmod(path, 384);
|
|
1468
|
+
}
|
|
1062
1469
|
async function reusablePending(directory, expected) {
|
|
1063
1470
|
let names;
|
|
1064
1471
|
try {
|
|
@@ -1455,7 +1862,7 @@ function createJsonSchemaValidator() {
|
|
|
1455
1862
|
return new AjvJsonSchemaValidator();
|
|
1456
1863
|
}
|
|
1457
1864
|
function registerTinyCloudTools(server, selection, validator) {
|
|
1458
|
-
const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface));
|
|
1865
|
+
const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface) && (selection.allowedTools === void 0 || selection.allowedTools.includes(binding.name)));
|
|
1459
1866
|
for (const binding of bindings) {
|
|
1460
1867
|
const operation = definitions.get(`${binding.operationId}@${binding.operationVersion}`);
|
|
1461
1868
|
if (operation === void 0) {
|
|
@@ -1533,7 +1940,7 @@ function outcomeCode(result) {
|
|
|
1533
1940
|
// package.json
|
|
1534
1941
|
var package_default = {
|
|
1535
1942
|
name: "@tinycloud/mcp",
|
|
1536
|
-
version: "0.8.3-beta.
|
|
1943
|
+
version: "0.8.3-beta.17",
|
|
1537
1944
|
description: "TinyCloud delegated operations MCP server",
|
|
1538
1945
|
keywords: [
|
|
1539
1946
|
"tinycloud",
|
|
@@ -1630,13 +2037,19 @@ var MCP_SERVER_NAME = "tinycloud-mcp";
|
|
|
1630
2037
|
|
|
1631
2038
|
// src/http.ts
|
|
1632
2039
|
var REQUIRED_SCOPE = "tinycloud:mcp";
|
|
2040
|
+
var LOCAL_CLI_PACKAGE = ["@tinycloud", "cli"].join("/");
|
|
2041
|
+
var LOCAL_MCP_PACKAGE = ["@tinycloud", "mcp"].join("/");
|
|
1633
2042
|
function createHostedMcpApp(config) {
|
|
2043
|
+
if (config.nativeServiceCredential !== void 0 && Buffer.byteLength(config.nativeServiceCredential, "utf8") < 32) {
|
|
2044
|
+
throw new Error("TC_MCP_NATIVE_SERVICE_CREDENTIAL must contain at least 32 bytes.");
|
|
2045
|
+
}
|
|
1634
2046
|
const resourceUrl = canonicalResourceUrl(config.publicUrl);
|
|
1635
2047
|
const pluginUrl = new URL("./chatgpt/mcp", resourceUrl);
|
|
1636
2048
|
const statusFor = (resource) => config.introspection === void 0 ? void 0 : createTokenStatusChecker({ ...config.introspection, resourceUrl: resource.toString() });
|
|
1637
2049
|
const hosted = createHostedSurface(config, resourceUrl, "hosted", statusFor(resourceUrl));
|
|
1638
2050
|
const plugin = createHostedSurface({
|
|
1639
2051
|
...config,
|
|
2052
|
+
nativeServiceCredential: void 0,
|
|
1640
2053
|
stateDir: join3(config.stateDir, "chatgpt"),
|
|
1641
2054
|
approvalPath: new URL("./connect", pluginUrl).pathname
|
|
1642
2055
|
}, pluginUrl, "public", statusFor(pluginUrl));
|
|
@@ -1672,16 +2085,19 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
|
|
|
1672
2085
|
resourceMetadataUrl
|
|
1673
2086
|
});
|
|
1674
2087
|
const store = new RemoteTenantStore({ ...config, publicUrl: resourceUrl });
|
|
2088
|
+
const nativeServiceIdentityEnabled = surface === "hosted" && config.nativeServiceCredential !== void 0;
|
|
2089
|
+
if (nativeServiceIdentityEnabled) void store.nativeServiceIdentity().catch(() => void 0);
|
|
1675
2090
|
const handler = createMcpHandler(async ({ authInfo: authInfo2 }) => {
|
|
1676
|
-
const
|
|
2091
|
+
const nativeOwner = typeof authInfo2?.extra?.nativeOwnerDid === "string" ? authInfo2.extra.nativeOwnerDid : void 0;
|
|
2092
|
+
const identity = nativeOwner === void 0 ? oauthIdentity(authInfo2) : { subject: `native:${nativeOwner}`, ownerDids: [nativeOwner] };
|
|
1677
2093
|
const { subject, ownerDids } = identity;
|
|
1678
|
-
const stateRoot = await store.initializeTenant(subject);
|
|
2094
|
+
const stateRoot = nativeOwner === void 0 ? await store.initializeTenant(subject) : store.nativeTenantStateRoot(nativeOwner);
|
|
1679
2095
|
const validator = createJsonSchemaValidator();
|
|
1680
2096
|
const server = new McpServer2(
|
|
1681
2097
|
{ name: MCP_SERVER_NAME, version: MCP_VERSION },
|
|
1682
2098
|
{
|
|
1683
2099
|
jsonSchemaValidator: validator,
|
|
1684
|
-
instructions: surface === "public" ? "Use TinyCloud only for the user's requested task. Call tinycloud_connect first. If an operation returns authority_required, present approval.url and wait for the user's approval before retrying. If approval is declined, stop that operation. For personal records, call tinycloud_apps_context first. Never request or store passwords, API keys, authentication codes, government identifiers, payment-card data, or protected health information." : "Call tinycloud_connect first. If an operation returns authority_required, open approval.url, then retry that operation unchanged. For personal records such as weight, workouts, goals or todos, call tinycloud_apps_context first."
|
|
2100
|
+
instructions: nativeOwner === void 0 ? surface === "public" ? "Use TinyCloud only for the user's requested task. Call tinycloud_connect first. If an operation returns authority_required, present approval.url and wait for the user's approval before retrying. If approval is declined, stop that operation. For personal records, call tinycloud_apps_context first. Never request or store passwords, API keys, authentication codes, government identifiers, payment-card data, or protected health information." : "Call tinycloud_connect first. If an operation returns authority_required, open approval.url, then retry that operation unchanged. For personal records such as weight, workouts, goals or todos, call tinycloud_apps_context first." : "Use only the user's requested connector-transcript data. The signed TinyCloud grant authorizes read-only SQL and transcript KV reads."
|
|
1685
2101
|
}
|
|
1686
2102
|
);
|
|
1687
2103
|
registerTinyCloudTools(server, {
|
|
@@ -1690,10 +2106,15 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
|
|
|
1690
2106
|
explicitProfile: true,
|
|
1691
2107
|
allowOwnerProfile: false,
|
|
1692
2108
|
stateRoot,
|
|
1693
|
-
|
|
1694
|
-
|
|
2109
|
+
...nativeOwner === void 0 ? {} : {
|
|
2110
|
+
allowedTools: ["tinycloud_sql_query", "tinycloud_kv_get"]
|
|
2111
|
+
},
|
|
2112
|
+
beforeOperation: nativeOwner === void 0 ? () => store.validateTenant(subject, ownerDids) : () => store.assertNativeGrantActive(nativeOwner),
|
|
2113
|
+
...nativeOwner === void 0 ? {
|
|
2114
|
+
transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
|
|
2115
|
+
} : {}
|
|
1695
2116
|
}, validator);
|
|
1696
|
-
registerConnectTool(server, validator, store, identity, surface);
|
|
2117
|
+
if (nativeOwner === void 0) registerConnectTool(server, validator, store, identity, surface);
|
|
1697
2118
|
return server;
|
|
1698
2119
|
}, {
|
|
1699
2120
|
legacy: "stateless",
|
|
@@ -1709,14 +2130,205 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
|
|
|
1709
2130
|
const allowedHosts = [resourceUrl.hostname];
|
|
1710
2131
|
return {
|
|
1711
2132
|
async fetch(request) {
|
|
1712
|
-
const metadata = oauthMetadataResponse(request, metadataOptions);
|
|
1713
|
-
if (metadata !== void 0) return metadata;
|
|
1714
2133
|
const url = new URL(request.url);
|
|
2134
|
+
if (url.pathname !== "/healthz") {
|
|
2135
|
+
const hostFailure = hostHeaderValidationResponse(request, allowedHosts);
|
|
2136
|
+
if (hostFailure !== void 0) return hostFailure;
|
|
2137
|
+
}
|
|
2138
|
+
if (url.pathname === "/.well-known/oauth-protected-resource" && surface === "hosted") {
|
|
2139
|
+
const metadataUrl = new URL(getOAuthProtectedResourceMetadataUrl(resourceUrl));
|
|
2140
|
+
const metadataRequest = new Request(metadataUrl.toString(), {
|
|
2141
|
+
method: request.method,
|
|
2142
|
+
headers: request.headers
|
|
2143
|
+
});
|
|
2144
|
+
const response = oauthMetadataResponse(metadataRequest, metadataOptions);
|
|
2145
|
+
if (response !== void 0) return withBearerMethodsSupported(response);
|
|
2146
|
+
}
|
|
2147
|
+
const metadata = oauthMetadataResponse(request, metadataOptions);
|
|
2148
|
+
if (metadata !== void 0) {
|
|
2149
|
+
return surface === "hosted" ? withBearerMethodsSupported(metadata) : metadata;
|
|
2150
|
+
}
|
|
2151
|
+
if (url.pathname === "/.well-known/ai-catalog.json" && surface === "hosted") {
|
|
2152
|
+
if (request.method === "OPTIONS") {
|
|
2153
|
+
return publicMetadataCors(new Response(null, { status: 204 }), request);
|
|
2154
|
+
}
|
|
2155
|
+
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
2156
|
+
return publicMetadataCors(Response.json({
|
|
2157
|
+
specVersion: "1.0",
|
|
2158
|
+
host: {
|
|
2159
|
+
displayName: "TinyCloud, Inc.",
|
|
2160
|
+
identifier: "https://mcp.tinycloud.xyz",
|
|
2161
|
+
documentationUrl: "https://docs.tinycloud.xyz/cli/mcp"
|
|
2162
|
+
},
|
|
2163
|
+
entries: [{
|
|
2164
|
+
identifier: "urn:air:tinycloud.xyz:mcp:tinycloud-mcp",
|
|
2165
|
+
displayName: "TinyCloud MCP",
|
|
2166
|
+
type: "application/json",
|
|
2167
|
+
description: "Hosted Streamable HTTP MCP server for delegated TinyCloud operations.",
|
|
2168
|
+
url: new URL("/.well-known/mcp/server-card.json", resourceUrl).toString(),
|
|
2169
|
+
representativeQueries: [
|
|
2170
|
+
"How do I connect to TinyCloud's hosted MCP server?",
|
|
2171
|
+
"Which endpoint provides TinyCloud's delegated MCP operations?",
|
|
2172
|
+
"How can I connect to TinyCloud MCP using local stdio?"
|
|
2173
|
+
]
|
|
2174
|
+
}]
|
|
2175
|
+
}, {
|
|
2176
|
+
headers: {
|
|
2177
|
+
"cache-control": "public, max-age=300",
|
|
2178
|
+
"content-type": "application/json; charset=utf-8"
|
|
2179
|
+
}
|
|
2180
|
+
}), request);
|
|
2181
|
+
}
|
|
2182
|
+
if (url.pathname === "/.well-known/poppy.json" && surface === "hosted") {
|
|
2183
|
+
if (request.method === "OPTIONS") {
|
|
2184
|
+
return publicMetadataCors(new Response(null, { status: 204 }), request);
|
|
2185
|
+
}
|
|
2186
|
+
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
2187
|
+
return publicMetadataCors(Response.json({
|
|
2188
|
+
protocol_version: "0.1",
|
|
2189
|
+
organization: {
|
|
2190
|
+
name: "TinyCloud, Inc.",
|
|
2191
|
+
domain: resourceUrl.hostname
|
|
2192
|
+
},
|
|
2193
|
+
web: {}
|
|
2194
|
+
}, {
|
|
2195
|
+
headers: {
|
|
2196
|
+
"cache-control": "public, max-age=300",
|
|
2197
|
+
"content-type": "application/json; charset=utf-8"
|
|
2198
|
+
}
|
|
2199
|
+
}), request);
|
|
2200
|
+
}
|
|
2201
|
+
if (url.pathname === "/auth.md" && surface === "hosted") {
|
|
2202
|
+
if (request.method === "OPTIONS") {
|
|
2203
|
+
return publicMetadataCors(new Response(null, { status: 204 }), request);
|
|
2204
|
+
}
|
|
2205
|
+
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
2206
|
+
const issuerUrl = new URL(issuer);
|
|
2207
|
+
const authorizationServerMetadataUrl = new URL(
|
|
2208
|
+
`/.well-known/oauth-authorization-server${issuerUrl.pathname}`,
|
|
2209
|
+
issuerUrl.origin
|
|
2210
|
+
);
|
|
2211
|
+
const registrationEndpoint = config.oauthMetadata.registration_endpoint;
|
|
2212
|
+
const registrationNote = typeof registrationEndpoint === "string" ? `OpenKey's OAuth \`registration_endpoint\` is ${registrationEndpoint}; it registers OAuth clients, not TinyCloud users or agents.` : "The authorization server does not advertise an OAuth client registration endpoint.";
|
|
2213
|
+
const authDoc = [
|
|
2214
|
+
"# TinyCloud MCP auth.md",
|
|
2215
|
+
"",
|
|
2216
|
+
`Protected resource: ${resourceUrl}.`,
|
|
2217
|
+
"",
|
|
2218
|
+
"Send an OAuth access token in the `Authorization: Bearer` header.",
|
|
2219
|
+
"",
|
|
2220
|
+
`Protected Resource Metadata: ${new URL("/.well-known/oauth-protected-resource", resourceUrl)}.`,
|
|
2221
|
+
`Authorization server issuer: ${issuer}.`,
|
|
2222
|
+
`Authorization server metadata: ${authorizationServerMetadataUrl}.`,
|
|
2223
|
+
`Scope: \`${REQUIRED_SCOPE}\`.`,
|
|
2224
|
+
"",
|
|
2225
|
+
"Hosted access uses OpenKey OAuth.",
|
|
2226
|
+
registrationNote,
|
|
2227
|
+
"TinyCloud does not host an Auth.md agent-enrollment or user-registration endpoint for this service.",
|
|
2228
|
+
"No `agent_auth` registration block is published because no agent-registration endpoint is available.",
|
|
2229
|
+
"",
|
|
2230
|
+
`Local stdio is a separate option configured with \`${LOCAL_CLI_PACKAGE}\` and \`${LOCAL_MCP_PACKAGE}\`.`,
|
|
2231
|
+
"",
|
|
2232
|
+
"Connection steps: https://docs.tinycloud.xyz/cli/mcp"
|
|
2233
|
+
].join("\n") + "\n";
|
|
2234
|
+
return publicMetadataCors(publicText(authDoc, "text/markdown; charset=utf-8"), request);
|
|
2235
|
+
}
|
|
2236
|
+
if (url.pathname === "/robots.txt" && surface === "hosted") {
|
|
2237
|
+
return request.method === "GET" ? publicText(`User-agent: *
|
|
2238
|
+
Allow: /
|
|
2239
|
+
Sitemap: ${new URL("/sitemap.xml", resourceUrl)}
|
|
2240
|
+
Agentmap: ${new URL("/.well-known/ai-catalog.json", resourceUrl)}
|
|
2241
|
+
`, "text/plain; charset=utf-8") : methodNotAllowed("GET");
|
|
2242
|
+
}
|
|
2243
|
+
if ((url.pathname === "/mcp/server-card" || url.pathname === "/.well-known/mcp/server-card.json") && surface === "hosted") {
|
|
2244
|
+
if (request.method === "OPTIONS") {
|
|
2245
|
+
return publicMetadataCors(new Response(null, { status: 204 }), request);
|
|
2246
|
+
}
|
|
2247
|
+
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
2248
|
+
const identity = nativeServiceIdentityEnabled ? await store.nativeServiceIdentity().catch(() => void 0) : void 0;
|
|
2249
|
+
const card = {
|
|
2250
|
+
$schema: "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json",
|
|
2251
|
+
name: `xyz.tinycloud/${MCP_SERVER_NAME}`,
|
|
2252
|
+
title: "TinyCloud MCP",
|
|
2253
|
+
version: MCP_VERSION,
|
|
2254
|
+
description: "MCP server for delegated TinyCloud operations.",
|
|
2255
|
+
websiteUrl: "https://docs.tinycloud.xyz/cli/mcp",
|
|
2256
|
+
repository: { source: "github", url: "https://github.com/TinyCloudLabs/js-sdk", subfolder: "packages/mcp" },
|
|
2257
|
+
remotes: [{ type: "streamable-http", url: resourceUrl.toString() }]
|
|
2258
|
+
};
|
|
2259
|
+
if (identity !== void 0) {
|
|
2260
|
+
card["x-tinycloud-native-auth"] = {
|
|
2261
|
+
serviceDid: identity.serviceDid,
|
|
2262
|
+
serviceDidField: "x-tinycloud-native-auth.serviceDid",
|
|
2263
|
+
grantEndpoint: {
|
|
2264
|
+
method: "POST",
|
|
2265
|
+
path: "/native/grant",
|
|
2266
|
+
body: { delegation: "serialized PortableDelegation JSON string" }
|
|
2267
|
+
},
|
|
2268
|
+
serviceCredentialHeader: NATIVE_SERVICE_CREDENTIAL_HEADER,
|
|
2269
|
+
serviceCredentialEnv: "TC_MCP_NATIVE_SERVICE_CREDENTIAL",
|
|
2270
|
+
ownerDidHeader: NATIVE_OWNER_DID_HEADER,
|
|
2271
|
+
maxGrantLifetimeSeconds: NATIVE_GRANT_MAX_LIFETIME_SECONDS,
|
|
2272
|
+
acceptedCapabilities: [
|
|
2273
|
+
{ service: "tinycloud.sql", space: "applications", path: "xyz.tinycloud.tinychat/connectors", actions: ["tinycloud.sql/read"] },
|
|
2274
|
+
{ service: "tinycloud.kv", space: "applications", path: "xyz.tinycloud.tinychat/connectors/", actions: ["tinycloud.kv/get", "tinycloud.kv/list"] }
|
|
2275
|
+
],
|
|
2276
|
+
tools: ["tinycloud_sql_query", "tinycloud_kv_get"],
|
|
2277
|
+
errors: {
|
|
2278
|
+
required: NATIVE_GRANT_REQUIRED,
|
|
2279
|
+
expired: NATIVE_GRANT_EXPIRED,
|
|
2280
|
+
rejected: NATIVE_GRANT_REJECTED,
|
|
2281
|
+
serviceUnauthorized: NATIVE_SERVICE_UNAUTHORIZED,
|
|
2282
|
+
serviceUnavailable: NATIVE_SERVICE_UNAVAILABLE,
|
|
2283
|
+
validationUnavailable: NATIVE_GRANT_VALIDATION_UNAVAILABLE
|
|
2284
|
+
}
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
return publicMetadataCors(Response.json(card, {
|
|
2288
|
+
headers: { "cache-control": "public, max-age=300" }
|
|
2289
|
+
}), request);
|
|
2290
|
+
}
|
|
2291
|
+
if (url.pathname === "/llms.txt" && surface === "hosted") {
|
|
2292
|
+
return request.method === "GET" ? publicText(`# TinyCloud MCP
|
|
2293
|
+
|
|
2294
|
+
Hosted Streamable HTTP endpoint: https://mcp.tinycloud.xyz/mcp (OpenKey OAuth).
|
|
2295
|
+
|
|
2296
|
+
Connection documentation: https://docs.tinycloud.xyz/cli/mcp
|
|
2297
|
+
|
|
2298
|
+
Local alternative: install ${LOCAL_CLI_PACKAGE} and ${LOCAL_MCP_PACKAGE}, then set up tinycloud-mcp as a stdio server in your MCP client.
|
|
2299
|
+
`, "text/plain; charset=utf-8") : methodNotAllowed("GET");
|
|
2300
|
+
}
|
|
2301
|
+
if (url.pathname === "/sitemap.xml" && surface === "hosted") {
|
|
2302
|
+
const sitemap = `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><url><loc>${new URL("/", resourceUrl)}</loc></url></urlset>`;
|
|
2303
|
+
return request.method === "GET" ? publicText(sitemap, "application/xml; charset=utf-8") : methodNotAllowed("GET");
|
|
2304
|
+
}
|
|
2305
|
+
if (url.pathname === "/" && surface === "hosted") {
|
|
2306
|
+
if (request.method !== "GET" && request.method !== "HEAD") return methodNotAllowed("GET, HEAD");
|
|
2307
|
+
const markdown = request.headers.get("accept")?.includes("text/markdown") ?? false;
|
|
2308
|
+
const content = markdown ? `# TinyCloud MCP
|
|
2309
|
+
|
|
2310
|
+
Hosted endpoint: <https://mcp.tinycloud.xyz/mcp> (Streamable HTTP, OpenKey OAuth).
|
|
2311
|
+
|
|
2312
|
+
[Connection documentation](https://docs.tinycloud.xyz/cli/mcp).
|
|
2313
|
+
|
|
2314
|
+
Local alternative: install \`${LOCAL_CLI_PACKAGE}\` and \`${LOCAL_MCP_PACKAGE}\` and connect the \`tinycloud-mcp\` stdio server.
|
|
2315
|
+
` : `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="ai-catalog" href="/.well-known/ai-catalog.json"><title>TinyCloud MCP</title></head><main><h1>TinyCloud MCP</h1><p>Hosted MCP endpoint: <code>https://mcp.tinycloud.xyz/mcp</code> (Streamable HTTP, OpenKey OAuth).</p><p><a href="https://docs.tinycloud.xyz/cli/mcp">Connection documentation</a></p><p>Local alternative: install <code>${LOCAL_CLI_PACKAGE}</code> and <code>${LOCAL_MCP_PACKAGE}</code> and connect the <code>tinycloud-mcp</code> stdio server.</p></main></html>`;
|
|
2316
|
+
const headers = new Headers({
|
|
2317
|
+
"content-type": markdown ? "text/markdown; charset=utf-8" : "text/html; charset=utf-8",
|
|
2318
|
+
"cache-control": "public, max-age=300",
|
|
2319
|
+
"content-security-policy": "default-src 'none'; base-uri 'none'; frame-ancestors 'none'",
|
|
2320
|
+
"x-content-type-options": "nosniff",
|
|
2321
|
+
vary: "Accept"
|
|
2322
|
+
});
|
|
2323
|
+
headers.set(
|
|
2324
|
+
"link",
|
|
2325
|
+
`<${new URL("/mcp/server-card", resourceUrl)}>; rel="describedby"; type="application/json", <https://docs.tinycloud.xyz/cli/mcp>; rel="service-doc"`
|
|
2326
|
+
);
|
|
2327
|
+
return new Response(request.method === "HEAD" ? null : content, { headers });
|
|
2328
|
+
}
|
|
1715
2329
|
if (url.pathname === "/healthz") {
|
|
1716
2330
|
return request.method === "GET" ? json({ ok: true }) : methodNotAllowed("GET");
|
|
1717
2331
|
}
|
|
1718
|
-
const hostFailure = hostHeaderValidationResponse(request, allowedHosts);
|
|
1719
|
-
if (hostFailure !== void 0) return hostFailure;
|
|
1720
2332
|
if (url.pathname === connectPath) {
|
|
1721
2333
|
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
1722
2334
|
try {
|
|
@@ -1772,6 +2384,73 @@ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
|
|
|
1772
2384
|
);
|
|
1773
2385
|
}
|
|
1774
2386
|
}
|
|
2387
|
+
if (url.pathname === "/native/grant" && surface === "hosted") {
|
|
2388
|
+
if (request.method !== "POST") return methodNotAllowed("POST");
|
|
2389
|
+
if (request.headers.has("origin")) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2390
|
+
if (config.nativeServiceCredential === void 0) {
|
|
2391
|
+
return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
|
|
2392
|
+
}
|
|
2393
|
+
if (!serviceCredentialMatches(
|
|
2394
|
+
request.headers.get(NATIVE_SERVICE_CREDENTIAL_HEADER),
|
|
2395
|
+
config.nativeServiceCredential
|
|
2396
|
+
)) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2397
|
+
if (!request.headers.get("content-type")?.toLowerCase().startsWith("application/json")) {
|
|
2398
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2399
|
+
}
|
|
2400
|
+
const bodyText = await readBoundedBodyText(request, MAX_NATIVE_GRANT_BODY_BYTES);
|
|
2401
|
+
if (bodyText === void 0) return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2402
|
+
let body;
|
|
2403
|
+
try {
|
|
2404
|
+
body = JSON.parse(bodyText);
|
|
2405
|
+
} catch {
|
|
2406
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2407
|
+
}
|
|
2408
|
+
if (typeof body !== "object" || body === null || Array.isArray(body) || !("delegation" in body)) {
|
|
2409
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2410
|
+
}
|
|
2411
|
+
const grantBody = body;
|
|
2412
|
+
if (Object.keys(body).length !== 1 || typeof grantBody.delegation !== "string") {
|
|
2413
|
+
return json({ error: NATIVE_GRANT_REJECTED }, 401);
|
|
2414
|
+
}
|
|
2415
|
+
try {
|
|
2416
|
+
const accepted = await store.acceptNativeGrant(grantBody.delegation);
|
|
2417
|
+
return json({
|
|
2418
|
+
ownerDid: accepted.ownerDid,
|
|
2419
|
+
serviceDid: accepted.serviceDid,
|
|
2420
|
+
expiresAt: accepted.expiresAt
|
|
2421
|
+
}, 201);
|
|
2422
|
+
} catch (error) {
|
|
2423
|
+
return nativeGrantErrorResponse(error);
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
if (url.pathname === resourceUrl.pathname && surface === "hosted" && (request.headers.has(NATIVE_SERVICE_CREDENTIAL_HEADER) || request.headers.has(NATIVE_OWNER_DID_HEADER))) {
|
|
2427
|
+
if (config.nativeServiceCredential === void 0) {
|
|
2428
|
+
return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
|
|
2429
|
+
}
|
|
2430
|
+
if (!serviceCredentialMatches(
|
|
2431
|
+
request.headers.get(NATIVE_SERVICE_CREDENTIAL_HEADER),
|
|
2432
|
+
config.nativeServiceCredential
|
|
2433
|
+
)) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2434
|
+
if (request.headers.has("origin")) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
|
|
2435
|
+
const ownerHeader = request.headers.get(NATIVE_OWNER_DID_HEADER);
|
|
2436
|
+
if (ownerHeader === null || ownerHeader.length === 0) {
|
|
2437
|
+
return json({ error: NATIVE_GRANT_REQUIRED }, 401);
|
|
2438
|
+
}
|
|
2439
|
+
const grant = await store.nativeGrantForOwner(ownerHeader);
|
|
2440
|
+
if (grant.status !== "ok") {
|
|
2441
|
+
const status = grant.status === "unavailable" ? 503 : 401;
|
|
2442
|
+
const code = grant.status === "required" ? NATIVE_GRANT_REQUIRED : grant.status === "expired" ? NATIVE_GRANT_EXPIRED : grant.status === "unavailable" ? NATIVE_GRANT_VALIDATION_UNAVAILABLE : NATIVE_GRANT_REJECTED;
|
|
2443
|
+
return json({ error: code }, status);
|
|
2444
|
+
}
|
|
2445
|
+
return handler.fetch(request, {
|
|
2446
|
+
authInfo: {
|
|
2447
|
+
token: "native-service",
|
|
2448
|
+
clientId: "tinycloud-native",
|
|
2449
|
+
scopes: [],
|
|
2450
|
+
extra: { nativeOwnerDid: grant.ownerDid }
|
|
2451
|
+
}
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
1775
2454
|
if (url.pathname !== resourceUrl.pathname) return json({ error: "Not found" }, 404);
|
|
1776
2455
|
const originFailure = validateOrigin(request, config.allowedOrigins);
|
|
1777
2456
|
if (originFailure !== void 0) return originFailure;
|
|
@@ -1883,9 +2562,77 @@ function callbackCors(response, openkeyHost) {
|
|
|
1883
2562
|
headers.set("vary", "Origin");
|
|
1884
2563
|
return new Response(response.body, { status: response.status, headers });
|
|
1885
2564
|
}
|
|
2565
|
+
async function readBoundedBodyText(request, maxBytes) {
|
|
2566
|
+
const contentLength = request.headers.get("content-length");
|
|
2567
|
+
if (contentLength !== null) {
|
|
2568
|
+
const parsedLength = Number(contentLength);
|
|
2569
|
+
if (Number.isFinite(parsedLength) && parsedLength > maxBytes) return void 0;
|
|
2570
|
+
}
|
|
2571
|
+
if (request.body === null) return "";
|
|
2572
|
+
const reader = request.body.getReader();
|
|
2573
|
+
const chunks = [];
|
|
2574
|
+
let total = 0;
|
|
2575
|
+
try {
|
|
2576
|
+
while (true) {
|
|
2577
|
+
const { done, value } = await reader.read();
|
|
2578
|
+
if (done) break;
|
|
2579
|
+
total += value.byteLength;
|
|
2580
|
+
if (total > maxBytes) {
|
|
2581
|
+
await reader.cancel();
|
|
2582
|
+
return void 0;
|
|
2583
|
+
}
|
|
2584
|
+
chunks.push(value);
|
|
2585
|
+
}
|
|
2586
|
+
} finally {
|
|
2587
|
+
reader.releaseLock();
|
|
2588
|
+
}
|
|
2589
|
+
return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
|
|
2590
|
+
}
|
|
2591
|
+
function nativeGrantErrorResponse(error) {
|
|
2592
|
+
if (!(error instanceof NativeGrantValidationError)) {
|
|
2593
|
+
return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
|
|
2594
|
+
}
|
|
2595
|
+
const unavailable = error.code === NATIVE_GRANT_VALIDATION_UNAVAILABLE || error.code === NATIVE_SERVICE_UNAVAILABLE;
|
|
2596
|
+
return json({ error: error.code }, unavailable ? 503 : 401);
|
|
2597
|
+
}
|
|
1886
2598
|
function json(value, status = 200) {
|
|
1887
2599
|
return Response.json(value, { status, headers: { "cache-control": "no-store" } });
|
|
1888
2600
|
}
|
|
2601
|
+
async function withBearerMethodsSupported(response) {
|
|
2602
|
+
if (response.status === 204 || response.body === null) return response;
|
|
2603
|
+
const metadata = await response.json();
|
|
2604
|
+
const headers = new Headers(response.headers);
|
|
2605
|
+
headers.delete("content-length");
|
|
2606
|
+
headers.delete("content-encoding");
|
|
2607
|
+
return new Response(JSON.stringify({ ...metadata, bearer_methods_supported: ["header"] }), {
|
|
2608
|
+
status: response.status,
|
|
2609
|
+
statusText: response.statusText,
|
|
2610
|
+
headers
|
|
2611
|
+
});
|
|
2612
|
+
}
|
|
2613
|
+
function publicMetadataCors(response, request) {
|
|
2614
|
+
const headers = new Headers(response.headers);
|
|
2615
|
+
headers.set("access-control-allow-origin", "*");
|
|
2616
|
+
headers.set("access-control-allow-methods", "GET, OPTIONS");
|
|
2617
|
+
const requestedHeaders = request.method === "OPTIONS" ? request.headers.get("access-control-request-headers") : null;
|
|
2618
|
+
if (requestedHeaders !== null) {
|
|
2619
|
+
headers.set("access-control-allow-headers", requestedHeaders);
|
|
2620
|
+
}
|
|
2621
|
+
headers.set(
|
|
2622
|
+
"vary",
|
|
2623
|
+
requestedHeaders === null ? "Origin" : "Origin, Access-Control-Request-Headers"
|
|
2624
|
+
);
|
|
2625
|
+
return new Response(response.body, { status: response.status, headers });
|
|
2626
|
+
}
|
|
2627
|
+
function publicText(text, contentType) {
|
|
2628
|
+
return new Response(text, {
|
|
2629
|
+
headers: {
|
|
2630
|
+
"content-type": contentType,
|
|
2631
|
+
"cache-control": "public, max-age=300",
|
|
2632
|
+
"x-content-type-options": "nosniff"
|
|
2633
|
+
}
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
1889
2636
|
function methodNotAllowed(allow) {
|
|
1890
2637
|
return new Response("Method not allowed", { status: 405, headers: { allow } });
|
|
1891
2638
|
}
|