@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/index.js
CHANGED
|
@@ -315,7 +315,7 @@ function registerTinyCloudTools(server, selection, validator) {
|
|
|
315
315
|
// package.json
|
|
316
316
|
var package_default = {
|
|
317
317
|
name: "@tinycloud/mcp",
|
|
318
|
-
version: "0.5.4-beta.
|
|
318
|
+
version: "0.5.4-beta.2",
|
|
319
319
|
description: "TinyCloud delegated operations MCP server",
|
|
320
320
|
keywords: [
|
|
321
321
|
"tinycloud",
|
|
@@ -388,7 +388,7 @@ var package_default = {
|
|
|
388
388
|
"@modelcontextprotocol/server": "2.0.0-beta.4",
|
|
389
389
|
"@tinycloud/node-sdk": "3.2.4-beta.0",
|
|
390
390
|
"@tinycloud/node-sdk-wasm": "1.7.7",
|
|
391
|
-
"@tinycloud/operations": "0.5.4-beta.
|
|
391
|
+
"@tinycloud/operations": "0.5.4-beta.1",
|
|
392
392
|
jose: "6.1.3"
|
|
393
393
|
},
|
|
394
394
|
devDependencies: {
|
|
@@ -536,6 +536,7 @@ import {
|
|
|
536
536
|
oauthMetadataResponse,
|
|
537
537
|
requireBearerAuth
|
|
538
538
|
} from "@modelcontextprotocol/server";
|
|
539
|
+
import { RUNTIME_DELEGATION_REJECTIONS } from "@tinycloud/node-sdk";
|
|
539
540
|
|
|
540
541
|
// src/oauth.ts
|
|
541
542
|
import {
|
|
@@ -622,7 +623,10 @@ import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
|
622
623
|
import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
|
|
623
624
|
import { dirname, join as join2 } from "path";
|
|
624
625
|
import {
|
|
626
|
+
InvalidRestoredSessionError,
|
|
627
|
+
RuntimeDelegationRejectedError,
|
|
625
628
|
TinyCloudNode,
|
|
629
|
+
UnsupportedSessionRestoreError,
|
|
626
630
|
isCapabilitySubset,
|
|
627
631
|
principalDidEquals,
|
|
628
632
|
siweRuntimeDelegation
|
|
@@ -631,6 +635,7 @@ import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk
|
|
|
631
635
|
import { invokeOperation as invokeOperation2 } from "@tinycloud/operations";
|
|
632
636
|
var PROFILE = "agent";
|
|
633
637
|
var MAX_CALLBACK_BYTES = 256 * 1024;
|
|
638
|
+
var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
|
|
634
639
|
var BOOTSTRAP_PERMISSIONS = [
|
|
635
640
|
{
|
|
636
641
|
service: "tinycloud.kv",
|
|
@@ -651,6 +656,26 @@ var ApprovalOwnerMismatchError = class extends Error {
|
|
|
651
656
|
this.name = "ApprovalOwnerMismatchError";
|
|
652
657
|
}
|
|
653
658
|
};
|
|
659
|
+
var ApprovalStageError = class extends Error {
|
|
660
|
+
stage;
|
|
661
|
+
code;
|
|
662
|
+
reason;
|
|
663
|
+
constructor(stage, code, reason, cause) {
|
|
664
|
+
super(`The approval failed at ${stage} (${code}).`, { cause });
|
|
665
|
+
this.name = "ApprovalStageError";
|
|
666
|
+
this.stage = stage;
|
|
667
|
+
this.code = code;
|
|
668
|
+
this.reason = reason;
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
var ApprovalRequestError = class extends Error {
|
|
672
|
+
detail;
|
|
673
|
+
constructor(detail, message, cause) {
|
|
674
|
+
super(message, { cause });
|
|
675
|
+
this.name = "ApprovalRequestError";
|
|
676
|
+
this.detail = detail;
|
|
677
|
+
}
|
|
678
|
+
};
|
|
654
679
|
var RemoteTenantStore = class {
|
|
655
680
|
#config;
|
|
656
681
|
#initializing = /* @__PURE__ */ new Map();
|
|
@@ -716,11 +741,20 @@ var RemoteTenantStore = class {
|
|
|
716
741
|
}
|
|
717
742
|
async completeApproval(state, request) {
|
|
718
743
|
const length = Number(request.headers.get("content-length") ?? "0");
|
|
719
|
-
if (Number.isFinite(length) && length > MAX_CALLBACK_BYTES)
|
|
744
|
+
if (Number.isFinite(length) && length > MAX_CALLBACK_BYTES) {
|
|
745
|
+
throw new ApprovalRequestError("body_invalid", "Callback body is too large.");
|
|
746
|
+
}
|
|
720
747
|
const text = await request.text();
|
|
721
|
-
if (Buffer.byteLength(text) > MAX_CALLBACK_BYTES)
|
|
722
|
-
|
|
723
|
-
|
|
748
|
+
if (Buffer.byteLength(text) > MAX_CALLBACK_BYTES) {
|
|
749
|
+
throw new ApprovalRequestError("body_invalid", "Callback body is too large.");
|
|
750
|
+
}
|
|
751
|
+
let data;
|
|
752
|
+
try {
|
|
753
|
+
data = JSON.parse(text);
|
|
754
|
+
} catch (error) {
|
|
755
|
+
throw new ApprovalRequestError("body_invalid", "Callback body is invalid.", error);
|
|
756
|
+
}
|
|
757
|
+
if (!isRecord(data)) throw new ApprovalRequestError("body_invalid", "Callback body is invalid.");
|
|
724
758
|
const pending = await this.#readPending(state, true);
|
|
725
759
|
try {
|
|
726
760
|
const tenant = await this.#readTenant(pending.tenantId);
|
|
@@ -758,16 +792,26 @@ var RemoteTenantStore = class {
|
|
|
758
792
|
if (pending.requestId === void 0) throw new Error("Authority request is missing.");
|
|
759
793
|
const { node, session } = await this.#restoreApproval(tenant, data);
|
|
760
794
|
if (!pending.ownerDids.some((did) => samePrincipal(node.did, did))) throw new ApprovalOwnerMismatchError();
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
795
|
+
let delegation;
|
|
796
|
+
try {
|
|
797
|
+
delegation = siweRuntimeDelegation(node, {
|
|
798
|
+
authorization: session.delegationHeader.Authorization,
|
|
799
|
+
cid: session.delegationCid,
|
|
800
|
+
spaceId: session.spaceId,
|
|
801
|
+
ownerAddress: session.address,
|
|
802
|
+
chainId: session.chainId,
|
|
803
|
+
siwe: session.siwe,
|
|
804
|
+
signature: session.signature,
|
|
805
|
+
host: this.#config.nodeHost
|
|
806
|
+
});
|
|
807
|
+
} catch (error) {
|
|
808
|
+
throw new ApprovalStageError(
|
|
809
|
+
"siwe_runtime_delegation",
|
|
810
|
+
"DELEGATION_REJECTED",
|
|
811
|
+
error instanceof RuntimeDelegationRejectedError ? error.reason : void 0,
|
|
812
|
+
error
|
|
813
|
+
);
|
|
814
|
+
}
|
|
771
815
|
const result = await invokeOperation2(
|
|
772
816
|
"tinycloud.auth.import",
|
|
773
817
|
1,
|
|
@@ -780,7 +824,15 @@ var RemoteTenantStore = class {
|
|
|
780
824
|
delegation
|
|
781
825
|
}
|
|
782
826
|
);
|
|
783
|
-
if (result.status
|
|
827
|
+
if (result.status === "error") {
|
|
828
|
+
const reason = result.error.details?.reason;
|
|
829
|
+
throw new ApprovalStageError(
|
|
830
|
+
result.error.code === "DELEGATION_REJECTED" && typeof reason === "string" ? "activation" : "auth_import",
|
|
831
|
+
result.error.code,
|
|
832
|
+
typeof reason === "string" ? reason : void 0
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
if (result.status !== "ok") throw new ApprovalStageError("auth_import", "AUTHORITY_REQUIRED");
|
|
784
836
|
}
|
|
785
837
|
/**
|
|
786
838
|
* Verify the OpenKey callback as a SIWE session for this tenant's key. The
|
|
@@ -790,11 +842,25 @@ var RemoteTenantStore = class {
|
|
|
790
842
|
* exactly the delegation being stored.
|
|
791
843
|
*/
|
|
792
844
|
async #restoreApproval(tenant, data) {
|
|
793
|
-
|
|
845
|
+
let session;
|
|
846
|
+
try {
|
|
847
|
+
session = sessionFromCallback(data, tenant.jwk);
|
|
848
|
+
} catch (error) {
|
|
849
|
+
throw new ApprovalStageError("restore_approval", "CALLBACK_INCOMPLETE", void 0, error);
|
|
850
|
+
}
|
|
794
851
|
const node = new TinyCloudNode({ host: this.#config.nodeHost });
|
|
795
|
-
|
|
852
|
+
try {
|
|
853
|
+
await node.restoreSession(session);
|
|
854
|
+
} catch (error) {
|
|
855
|
+
throw new ApprovalStageError(
|
|
856
|
+
"restore_approval",
|
|
857
|
+
error instanceof InvalidRestoredSessionError || error instanceof UnsupportedSessionRestoreError ? error.code : "SESSION_RESTORE_FAILED",
|
|
858
|
+
void 0,
|
|
859
|
+
error
|
|
860
|
+
);
|
|
861
|
+
}
|
|
796
862
|
if (!samePrincipal(node.sessionDid, tenant.sessionDid)) {
|
|
797
|
-
throw new
|
|
863
|
+
throw new ApprovalStageError("restore_approval", "SESSION_MISMATCH");
|
|
798
864
|
}
|
|
799
865
|
return { node, session };
|
|
800
866
|
}
|
|
@@ -835,7 +901,8 @@ var RemoteTenantStore = class {
|
|
|
835
901
|
sessionDid = didFromJwk(jwk);
|
|
836
902
|
}
|
|
837
903
|
const profilePath = this.#profilePath(stateRoot);
|
|
838
|
-
|
|
904
|
+
const profile = await readJson(profilePath);
|
|
905
|
+
if (profile === null) {
|
|
839
906
|
await secureWrite(profilePath, {
|
|
840
907
|
name: PROFILE,
|
|
841
908
|
host: this.#config.nodeHost,
|
|
@@ -849,14 +916,20 @@ var RemoteTenantStore = class {
|
|
|
849
916
|
authMethod: "openkey",
|
|
850
917
|
openkeyHost: this.#config.openkeyHost
|
|
851
918
|
});
|
|
919
|
+
} else if (typeof profile.host === "string" && profile.host.replace(/\/+$/, "") === LEGACY_DEFAULT_NODE_HOST && this.#config.nodeHost.replace(/\/+$/, "") !== LEGACY_DEFAULT_NODE_HOST) {
|
|
920
|
+
await secureWrite(profilePath, { ...profile, host: this.#config.nodeHost });
|
|
921
|
+
process.stderr.write(
|
|
922
|
+
`[tinycloud-mcp-http] tenant node host migrated: from=${LEGACY_DEFAULT_NODE_HOST} to=${new URL(this.#config.nodeHost).origin}
|
|
923
|
+
`
|
|
924
|
+
);
|
|
852
925
|
}
|
|
853
926
|
return { stateRoot, sessionDid, jwk };
|
|
854
927
|
}
|
|
855
928
|
async #readTenant(tenantId) {
|
|
856
|
-
if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new
|
|
929
|
+
if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
857
930
|
const stateRoot = join2(this.#config.stateDir, "tenants", tenantId);
|
|
858
931
|
const jwk = await readJson(this.#keyPath(stateRoot));
|
|
859
|
-
if (jwk === null) throw new
|
|
932
|
+
if (jwk === null) throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.");
|
|
860
933
|
return { stateRoot, jwk, sessionDid: didFromJwk(jwk) };
|
|
861
934
|
}
|
|
862
935
|
async #createPending(subject, ownerDids, kind, permissions, requestId) {
|
|
@@ -886,26 +959,40 @@ var RemoteTenantStore = class {
|
|
|
886
959
|
async #readPending(state, consume) {
|
|
887
960
|
const [tenantId, nonce, signature] = state.split(".");
|
|
888
961
|
if (tenantId === void 0 || nonce === void 0 || signature === void 0) {
|
|
889
|
-
throw new
|
|
962
|
+
throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
890
963
|
}
|
|
891
964
|
const expected = this.#signState(tenantId, nonce);
|
|
892
965
|
const supplied = Buffer.from(signature, "base64url");
|
|
893
966
|
const expectedBytes = Buffer.from(expected, "base64url");
|
|
894
967
|
if (supplied.length !== expectedBytes.length || !timingSafeEqual(supplied, expectedBytes)) {
|
|
895
|
-
throw new
|
|
968
|
+
throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
896
969
|
}
|
|
897
970
|
const stateRoot = join2(this.#config.stateDir, "tenants", tenantId);
|
|
898
971
|
const path = this.#pendingPath(stateRoot, nonce);
|
|
899
972
|
const claimed = `${path}.processing`;
|
|
900
|
-
if (consume)
|
|
973
|
+
if (consume) {
|
|
974
|
+
try {
|
|
975
|
+
await rename(path, claimed);
|
|
976
|
+
} catch (error) {
|
|
977
|
+
if (!isRecord(error) || error.code !== "ENOENT") throw error;
|
|
978
|
+
const tenantExists = await readJson(this.#keyPath(stateRoot)).then(
|
|
979
|
+
(jwk) => jwk !== null,
|
|
980
|
+
() => true
|
|
981
|
+
);
|
|
982
|
+
if (!tenantExists) {
|
|
983
|
+
throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.", error);
|
|
984
|
+
}
|
|
985
|
+
throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.", error);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
901
988
|
try {
|
|
902
989
|
const pending = await readJson(consume ? claimed : path);
|
|
903
990
|
if (pending === null || pending.version !== 1 || pending.tenantId !== tenantId || pending.nonce !== nonce) {
|
|
904
|
-
throw new
|
|
991
|
+
throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
|
|
905
992
|
}
|
|
906
993
|
if (new Date(pending.expiresAt).getTime() <= Date.now()) {
|
|
907
994
|
await rm(consume ? claimed : path, { force: true });
|
|
908
|
-
throw new
|
|
995
|
+
throw new ApprovalRequestError("state_expired", "Approval state has expired.");
|
|
909
996
|
}
|
|
910
997
|
return pending;
|
|
911
998
|
} catch (error) {
|
|
@@ -1028,9 +1115,6 @@ async function readJson(path) {
|
|
|
1028
1115
|
throw error;
|
|
1029
1116
|
}
|
|
1030
1117
|
}
|
|
1031
|
-
async function fileExists(path) {
|
|
1032
|
-
return await readJson(path) !== null;
|
|
1033
|
-
}
|
|
1034
1118
|
async function reusablePending(directory, expected) {
|
|
1035
1119
|
let names;
|
|
1036
1120
|
try {
|
|
@@ -1142,8 +1226,23 @@ function createHostedMcpApp(config) {
|
|
|
1142
1226
|
await store.completeApproval(requiredState(url), request);
|
|
1143
1227
|
return callbackCors(json({ success: true }), config.openkeyHost);
|
|
1144
1228
|
} catch (error) {
|
|
1229
|
+
if (error instanceof ApprovalOwnerMismatchError) {
|
|
1230
|
+
return callbackCors(json({ error: error.message, code: "approval_owner_mismatch" }, 403), config.openkeyHost);
|
|
1231
|
+
}
|
|
1232
|
+
const stage = error instanceof ApprovalStageError ? error.stage : "unknown";
|
|
1233
|
+
const code = error instanceof ApprovalStageError && /^[A-Z][A-Z0-9_]{0,63}$/.test(error.code) ? error.code : "UNKNOWN";
|
|
1234
|
+
const reason = !(error instanceof ApprovalStageError) || error.reason === void 0 ? void 0 : RUNTIME_DELEGATION_REJECTIONS.find((known) => known === error.reason) ?? "unknown";
|
|
1235
|
+
const detail = error instanceof ApprovalRequestError ? error.detail : "none";
|
|
1236
|
+
writeDiagnostic(
|
|
1237
|
+
`connect callback rejected: stage=${stage} code=${code}${reason === void 0 ? "" : ` reason=${reason}`} detail=${detail}`
|
|
1238
|
+
);
|
|
1145
1239
|
return callbackCors(
|
|
1146
|
-
|
|
1240
|
+
json({
|
|
1241
|
+
error: stage,
|
|
1242
|
+
code,
|
|
1243
|
+
...reason === void 0 ? {} : { reason },
|
|
1244
|
+
message: "TinyCloud could not accept this delegation."
|
|
1245
|
+
}, 400),
|
|
1147
1246
|
config.openkeyHost
|
|
1148
1247
|
);
|
|
1149
1248
|
}
|
|
@@ -1218,7 +1317,8 @@ function canonicalResourceUrl(url) {
|
|
|
1218
1317
|
}
|
|
1219
1318
|
function requiredState(url) {
|
|
1220
1319
|
const state = url.searchParams.get("state");
|
|
1221
|
-
if (state === null
|
|
1320
|
+
if (state === null) throw new ApprovalRequestError("state_missing", "Missing approval state.");
|
|
1321
|
+
if (state.length > 512) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
1222
1322
|
return state;
|
|
1223
1323
|
}
|
|
1224
1324
|
function validateOrigin(request, allowed) {
|