@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.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.0",
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.0",
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) throw new Error("Callback body is too large.");
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) throw new Error("Callback body is too large.");
722
- const data = JSON.parse(text);
723
- if (!isRecord(data)) throw new Error("Callback body is invalid.");
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
- const delegation = siweRuntimeDelegation(node, {
762
- authorization: session.delegationHeader.Authorization,
763
- cid: session.delegationCid,
764
- spaceId: session.spaceId,
765
- ownerAddress: session.address,
766
- chainId: session.chainId,
767
- siwe: session.siwe,
768
- signature: session.signature,
769
- host: this.#config.nodeHost
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 !== "ok") throw new Error("TinyCloud rejected the approved delegation.");
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
- const session = sessionFromCallback(data, tenant.jwk);
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
- await node.restoreSession(session);
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 Error("The approved delegation targets a different session.");
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
- if (!await fileExists(profilePath)) {
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 Error("Approval state is invalid.");
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 Error("The TinyCloud delegate no longer exists.");
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 Error("Approval state is invalid.");
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 Error("Approval state is invalid.");
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) await rename(path, claimed);
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 Error("Approval state is invalid or already used.");
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 Error("Approval state has expired.");
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
- error instanceof ApprovalOwnerMismatchError ? json({ error: error.message, code: "approval_owner_mismatch" }, 403) : json({ error: "TinyCloud could not accept this delegation." }, 400),
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 || state.length > 512) throw new Error("Missing approval state.");
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) {