@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/index.cjs CHANGED
@@ -352,7 +352,7 @@ function registerTinyCloudTools(server, selection, validator) {
352
352
  // package.json
353
353
  var package_default = {
354
354
  name: "@tinycloud/mcp",
355
- version: "0.5.4-beta.0",
355
+ version: "0.5.4-beta.2",
356
356
  description: "TinyCloud delegated operations MCP server",
357
357
  keywords: [
358
358
  "tinycloud",
@@ -425,7 +425,7 @@ var package_default = {
425
425
  "@modelcontextprotocol/server": "2.0.0-beta.4",
426
426
  "@tinycloud/node-sdk": "3.2.4-beta.0",
427
427
  "@tinycloud/node-sdk-wasm": "1.7.7",
428
- "@tinycloud/operations": "0.5.4-beta.0",
428
+ "@tinycloud/operations": "0.5.4-beta.1",
429
429
  jose: "6.1.3"
430
430
  },
431
431
  devDependencies: {
@@ -565,6 +565,7 @@ if (isDirectInvocation()) {
565
565
 
566
566
  // src/http.ts
567
567
  var import_server5 = require("@modelcontextprotocol/server");
568
+ var import_node_sdk3 = require("@tinycloud/node-sdk");
568
569
 
569
570
  // src/oauth.ts
570
571
  var import_server4 = require("@modelcontextprotocol/server");
@@ -652,6 +653,7 @@ var import_node_sdk_wasm = require("@tinycloud/node-sdk-wasm");
652
653
  var import_operations2 = require("@tinycloud/operations");
653
654
  var PROFILE = "agent";
654
655
  var MAX_CALLBACK_BYTES = 256 * 1024;
656
+ var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
655
657
  var BOOTSTRAP_PERMISSIONS = [
656
658
  {
657
659
  service: "tinycloud.kv",
@@ -672,6 +674,26 @@ var ApprovalOwnerMismatchError = class extends Error {
672
674
  this.name = "ApprovalOwnerMismatchError";
673
675
  }
674
676
  };
677
+ var ApprovalStageError = class extends Error {
678
+ stage;
679
+ code;
680
+ reason;
681
+ constructor(stage, code, reason, cause) {
682
+ super(`The approval failed at ${stage} (${code}).`, { cause });
683
+ this.name = "ApprovalStageError";
684
+ this.stage = stage;
685
+ this.code = code;
686
+ this.reason = reason;
687
+ }
688
+ };
689
+ var ApprovalRequestError = class extends Error {
690
+ detail;
691
+ constructor(detail, message, cause) {
692
+ super(message, { cause });
693
+ this.name = "ApprovalRequestError";
694
+ this.detail = detail;
695
+ }
696
+ };
675
697
  var RemoteTenantStore = class {
676
698
  #config;
677
699
  #initializing = /* @__PURE__ */ new Map();
@@ -737,11 +759,20 @@ var RemoteTenantStore = class {
737
759
  }
738
760
  async completeApproval(state, request) {
739
761
  const length = Number(request.headers.get("content-length") ?? "0");
740
- if (Number.isFinite(length) && length > MAX_CALLBACK_BYTES) throw new Error("Callback body is too large.");
762
+ if (Number.isFinite(length) && length > MAX_CALLBACK_BYTES) {
763
+ throw new ApprovalRequestError("body_invalid", "Callback body is too large.");
764
+ }
741
765
  const text = await request.text();
742
- if (Buffer.byteLength(text) > MAX_CALLBACK_BYTES) throw new Error("Callback body is too large.");
743
- const data = JSON.parse(text);
744
- if (!isRecord(data)) throw new Error("Callback body is invalid.");
766
+ if (Buffer.byteLength(text) > MAX_CALLBACK_BYTES) {
767
+ throw new ApprovalRequestError("body_invalid", "Callback body is too large.");
768
+ }
769
+ let data;
770
+ try {
771
+ data = JSON.parse(text);
772
+ } catch (error) {
773
+ throw new ApprovalRequestError("body_invalid", "Callback body is invalid.", error);
774
+ }
775
+ if (!isRecord(data)) throw new ApprovalRequestError("body_invalid", "Callback body is invalid.");
745
776
  const pending = await this.#readPending(state, true);
746
777
  try {
747
778
  const tenant = await this.#readTenant(pending.tenantId);
@@ -779,16 +810,26 @@ var RemoteTenantStore = class {
779
810
  if (pending.requestId === void 0) throw new Error("Authority request is missing.");
780
811
  const { node, session } = await this.#restoreApproval(tenant, data);
781
812
  if (!pending.ownerDids.some((did) => samePrincipal(node.did, did))) throw new ApprovalOwnerMismatchError();
782
- const delegation = (0, import_node_sdk2.siweRuntimeDelegation)(node, {
783
- authorization: session.delegationHeader.Authorization,
784
- cid: session.delegationCid,
785
- spaceId: session.spaceId,
786
- ownerAddress: session.address,
787
- chainId: session.chainId,
788
- siwe: session.siwe,
789
- signature: session.signature,
790
- host: this.#config.nodeHost
791
- });
813
+ let delegation;
814
+ try {
815
+ delegation = (0, import_node_sdk2.siweRuntimeDelegation)(node, {
816
+ authorization: session.delegationHeader.Authorization,
817
+ cid: session.delegationCid,
818
+ spaceId: session.spaceId,
819
+ ownerAddress: session.address,
820
+ chainId: session.chainId,
821
+ siwe: session.siwe,
822
+ signature: session.signature,
823
+ host: this.#config.nodeHost
824
+ });
825
+ } catch (error) {
826
+ throw new ApprovalStageError(
827
+ "siwe_runtime_delegation",
828
+ "DELEGATION_REJECTED",
829
+ error instanceof import_node_sdk2.RuntimeDelegationRejectedError ? error.reason : void 0,
830
+ error
831
+ );
832
+ }
792
833
  const result = await (0, import_operations2.invokeOperation)(
793
834
  "tinycloud.auth.import",
794
835
  1,
@@ -801,7 +842,15 @@ var RemoteTenantStore = class {
801
842
  delegation
802
843
  }
803
844
  );
804
- if (result.status !== "ok") throw new Error("TinyCloud rejected the approved delegation.");
845
+ if (result.status === "error") {
846
+ const reason = result.error.details?.reason;
847
+ throw new ApprovalStageError(
848
+ result.error.code === "DELEGATION_REJECTED" && typeof reason === "string" ? "activation" : "auth_import",
849
+ result.error.code,
850
+ typeof reason === "string" ? reason : void 0
851
+ );
852
+ }
853
+ if (result.status !== "ok") throw new ApprovalStageError("auth_import", "AUTHORITY_REQUIRED");
805
854
  }
806
855
  /**
807
856
  * Verify the OpenKey callback as a SIWE session for this tenant's key. The
@@ -811,11 +860,25 @@ var RemoteTenantStore = class {
811
860
  * exactly the delegation being stored.
812
861
  */
813
862
  async #restoreApproval(tenant, data) {
814
- const session = sessionFromCallback(data, tenant.jwk);
863
+ let session;
864
+ try {
865
+ session = sessionFromCallback(data, tenant.jwk);
866
+ } catch (error) {
867
+ throw new ApprovalStageError("restore_approval", "CALLBACK_INCOMPLETE", void 0, error);
868
+ }
815
869
  const node = new import_node_sdk2.TinyCloudNode({ host: this.#config.nodeHost });
816
- await node.restoreSession(session);
870
+ try {
871
+ await node.restoreSession(session);
872
+ } catch (error) {
873
+ throw new ApprovalStageError(
874
+ "restore_approval",
875
+ error instanceof import_node_sdk2.InvalidRestoredSessionError || error instanceof import_node_sdk2.UnsupportedSessionRestoreError ? error.code : "SESSION_RESTORE_FAILED",
876
+ void 0,
877
+ error
878
+ );
879
+ }
817
880
  if (!samePrincipal(node.sessionDid, tenant.sessionDid)) {
818
- throw new Error("The approved delegation targets a different session.");
881
+ throw new ApprovalStageError("restore_approval", "SESSION_MISMATCH");
819
882
  }
820
883
  return { node, session };
821
884
  }
@@ -856,7 +919,8 @@ var RemoteTenantStore = class {
856
919
  sessionDid = didFromJwk(jwk);
857
920
  }
858
921
  const profilePath = this.#profilePath(stateRoot);
859
- if (!await fileExists(profilePath)) {
922
+ const profile = await readJson(profilePath);
923
+ if (profile === null) {
860
924
  await secureWrite(profilePath, {
861
925
  name: PROFILE,
862
926
  host: this.#config.nodeHost,
@@ -870,14 +934,20 @@ var RemoteTenantStore = class {
870
934
  authMethod: "openkey",
871
935
  openkeyHost: this.#config.openkeyHost
872
936
  });
937
+ } else if (typeof profile.host === "string" && profile.host.replace(/\/+$/, "") === LEGACY_DEFAULT_NODE_HOST && this.#config.nodeHost.replace(/\/+$/, "") !== LEGACY_DEFAULT_NODE_HOST) {
938
+ await secureWrite(profilePath, { ...profile, host: this.#config.nodeHost });
939
+ process.stderr.write(
940
+ `[tinycloud-mcp-http] tenant node host migrated: from=${LEGACY_DEFAULT_NODE_HOST} to=${new URL(this.#config.nodeHost).origin}
941
+ `
942
+ );
873
943
  }
874
944
  return { stateRoot, sessionDid, jwk };
875
945
  }
876
946
  async #readTenant(tenantId) {
877
- if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new Error("Approval state is invalid.");
947
+ if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
878
948
  const stateRoot = (0, import_node_path3.join)(this.#config.stateDir, "tenants", tenantId);
879
949
  const jwk = await readJson(this.#keyPath(stateRoot));
880
- if (jwk === null) throw new Error("The TinyCloud delegate no longer exists.");
950
+ if (jwk === null) throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.");
881
951
  return { stateRoot, jwk, sessionDid: didFromJwk(jwk) };
882
952
  }
883
953
  async #createPending(subject, ownerDids, kind, permissions, requestId) {
@@ -907,26 +977,40 @@ var RemoteTenantStore = class {
907
977
  async #readPending(state, consume) {
908
978
  const [tenantId, nonce, signature] = state.split(".");
909
979
  if (tenantId === void 0 || nonce === void 0 || signature === void 0) {
910
- throw new Error("Approval state is invalid.");
980
+ throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
911
981
  }
912
982
  const expected = this.#signState(tenantId, nonce);
913
983
  const supplied = Buffer.from(signature, "base64url");
914
984
  const expectedBytes = Buffer.from(expected, "base64url");
915
985
  if (supplied.length !== expectedBytes.length || !(0, import_node_crypto.timingSafeEqual)(supplied, expectedBytes)) {
916
- throw new Error("Approval state is invalid.");
986
+ throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
917
987
  }
918
988
  const stateRoot = (0, import_node_path3.join)(this.#config.stateDir, "tenants", tenantId);
919
989
  const path = this.#pendingPath(stateRoot, nonce);
920
990
  const claimed = `${path}.processing`;
921
- if (consume) await (0, import_promises.rename)(path, claimed);
991
+ if (consume) {
992
+ try {
993
+ await (0, import_promises.rename)(path, claimed);
994
+ } catch (error) {
995
+ if (!isRecord(error) || error.code !== "ENOENT") throw error;
996
+ const tenantExists = await readJson(this.#keyPath(stateRoot)).then(
997
+ (jwk) => jwk !== null,
998
+ () => true
999
+ );
1000
+ if (!tenantExists) {
1001
+ throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.", error);
1002
+ }
1003
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.", error);
1004
+ }
1005
+ }
922
1006
  try {
923
1007
  const pending = await readJson(consume ? claimed : path);
924
1008
  if (pending === null || pending.version !== 1 || pending.tenantId !== tenantId || pending.nonce !== nonce) {
925
- throw new Error("Approval state is invalid or already used.");
1009
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
926
1010
  }
927
1011
  if (new Date(pending.expiresAt).getTime() <= Date.now()) {
928
1012
  await (0, import_promises.rm)(consume ? claimed : path, { force: true });
929
- throw new Error("Approval state has expired.");
1013
+ throw new ApprovalRequestError("state_expired", "Approval state has expired.");
930
1014
  }
931
1015
  return pending;
932
1016
  } catch (error) {
@@ -1049,9 +1133,6 @@ async function readJson(path) {
1049
1133
  throw error;
1050
1134
  }
1051
1135
  }
1052
- async function fileExists(path) {
1053
- return await readJson(path) !== null;
1054
- }
1055
1136
  async function reusablePending(directory, expected) {
1056
1137
  let names;
1057
1138
  try {
@@ -1163,8 +1244,23 @@ function createHostedMcpApp(config) {
1163
1244
  await store.completeApproval(requiredState(url), request);
1164
1245
  return callbackCors(json({ success: true }), config.openkeyHost);
1165
1246
  } catch (error) {
1247
+ if (error instanceof ApprovalOwnerMismatchError) {
1248
+ return callbackCors(json({ error: error.message, code: "approval_owner_mismatch" }, 403), config.openkeyHost);
1249
+ }
1250
+ const stage = error instanceof ApprovalStageError ? error.stage : "unknown";
1251
+ const code = error instanceof ApprovalStageError && /^[A-Z][A-Z0-9_]{0,63}$/.test(error.code) ? error.code : "UNKNOWN";
1252
+ const reason = !(error instanceof ApprovalStageError) || error.reason === void 0 ? void 0 : import_node_sdk3.RUNTIME_DELEGATION_REJECTIONS.find((known) => known === error.reason) ?? "unknown";
1253
+ const detail = error instanceof ApprovalRequestError ? error.detail : "none";
1254
+ writeDiagnostic(
1255
+ `connect callback rejected: stage=${stage} code=${code}${reason === void 0 ? "" : ` reason=${reason}`} detail=${detail}`
1256
+ );
1166
1257
  return callbackCors(
1167
- error instanceof ApprovalOwnerMismatchError ? json({ error: error.message, code: "approval_owner_mismatch" }, 403) : json({ error: "TinyCloud could not accept this delegation." }, 400),
1258
+ json({
1259
+ error: stage,
1260
+ code,
1261
+ ...reason === void 0 ? {} : { reason },
1262
+ message: "TinyCloud could not accept this delegation."
1263
+ }, 400),
1168
1264
  config.openkeyHost
1169
1265
  );
1170
1266
  }
@@ -1239,7 +1335,8 @@ function canonicalResourceUrl(url) {
1239
1335
  }
1240
1336
  function requiredState(url) {
1241
1337
  const state = url.searchParams.get("state");
1242
- if (state === null || state.length > 512) throw new Error("Missing approval state.");
1338
+ if (state === null) throw new ApprovalRequestError("state_missing", "Missing approval state.");
1339
+ if (state.length > 512) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
1243
1340
  return state;
1244
1341
  }
1245
1342
  function validateOrigin(request, allowed) {