@tinycloud/mcp 0.4.1-beta.1 → 0.5.0-beta.10

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
@@ -50,8 +50,8 @@ var import_ajv = require("@modelcontextprotocol/server/validators/ajv");
50
50
  var import_operations = require("@tinycloud/operations");
51
51
 
52
52
  // src/results.ts
53
- function toMcpToolResult(result) {
54
- const canonical = isCanonicalOperationResult(result) ? result : {
53
+ function toMcpToolResult(result, bindings = []) {
54
+ const canonical = isCanonicalOperationResult(result) ? withRecoveryTool(result, bindings) : {
55
55
  status: "error",
56
56
  operation: {},
57
57
  context: {},
@@ -62,6 +62,18 @@ function toMcpToolResult(result) {
62
62
  structuredContent: canonical
63
63
  };
64
64
  }
65
+ function withRecoveryTool(result, bindings) {
66
+ if (result.status !== "error") return result;
67
+ const error = result.error;
68
+ const recovery = error?.details?.recovery;
69
+ if (recovery?.action !== "complete_migration") return result;
70
+ const binding = bindings.find((candidate) => candidate.operationId === recovery.operationId);
71
+ if (binding === void 0) return result;
72
+ return {
73
+ ...result,
74
+ error: { ...error, details: { ...error?.details, recovery: { ...recovery, tool: binding.name } } }
75
+ };
76
+ }
65
77
  function canonicalMcpAdapterResult(result) {
66
78
  return toMcpToolResult(result).structuredContent;
67
79
  }
@@ -101,7 +113,8 @@ var TOOL_NAMES = [
101
113
  "tinycloud_sql_schema_inspect",
102
114
  "tinycloud_sql_query",
103
115
  "tinycloud_sql_execute",
104
- "tinycloud_secrets_get"
116
+ "tinycloud_secrets_get",
117
+ "tinycloud_secrets_status"
105
118
  ];
106
119
  function invocationTargetForMcp(selection) {
107
120
  return {
@@ -116,6 +129,10 @@ var packageRequire = (0, import_node_module.createRequire)(
116
129
  var generatedCatalog = packageRequire(
117
130
  "@tinycloud/operations/operations.json"
118
131
  );
132
+ var ALL_SURFACES = ["local", "hosted"];
133
+ function bindingSurfaces(binding) {
134
+ return binding.surfaces ?? ALL_SURFACES;
135
+ }
119
136
  var TOOL_BINDINGS = [
120
137
  {
121
138
  name: "tinycloud_status",
@@ -160,7 +177,10 @@ var TOOL_BINDINGS = [
160
177
  readOnlyHint: false,
161
178
  idempotentHint: true,
162
179
  destructiveHint: false,
163
- openWorldHint: false
180
+ openWorldHint: false,
181
+ // Hosted delegations must arrive through the owner/request-bound OpenKey callback,
182
+ // not a caller-supplied import artifact.
183
+ surfaces: ["local"]
164
184
  },
165
185
  {
166
186
  name: "tinycloud_account_spaces_list",
@@ -260,6 +280,18 @@ var TOOL_BINDINGS = [
260
280
  idempotentHint: true,
261
281
  destructiveHint: false,
262
282
  openWorldHint: true
283
+ },
284
+ {
285
+ name: "tinycloud_secrets_status",
286
+ operationId: "tinycloud.secrets.status",
287
+ operationVersion: 1,
288
+ readOnlyHint: true,
289
+ idempotentHint: true,
290
+ destructiveHint: false,
291
+ openWorldHint: true,
292
+ // Hosted has no persistent reauthorization path for a revoked session
293
+ // yet (TC-896), so status is local/stdio only.
294
+ surfaces: ["local"]
263
295
  }
264
296
  ];
265
297
  function createJsonSchemaValidator() {
@@ -275,6 +307,7 @@ function registerTinyCloudTools(server, selection, validator) {
275
307
  ])
276
308
  );
277
309
  for (const binding of TOOL_BINDINGS) {
310
+ if (!bindingSurfaces(binding).includes(selection.surface)) continue;
278
311
  const operation = definitions.get(`${binding.operationId}@${binding.operationVersion}`);
279
312
  if (operation === void 0) {
280
313
  throw new Error(`Generated operations catalog is missing ${binding.operationId}@${binding.operationVersion}.`);
@@ -310,7 +343,7 @@ function registerTinyCloudTools(server, selection, validator) {
310
343
  input
311
344
  );
312
345
  const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
313
- return toMcpToolResult(projected);
346
+ return toMcpToolResult(projected, TOOL_BINDINGS);
314
347
  }
315
348
  );
316
349
  }
@@ -319,7 +352,7 @@ function registerTinyCloudTools(server, selection, validator) {
319
352
  // package.json
320
353
  var package_default = {
321
354
  name: "@tinycloud/mcp",
322
- version: "0.4.1-beta.1",
355
+ version: "0.5.0-beta.10",
323
356
  description: "TinyCloud delegated operations MCP server",
324
357
  keywords: [
325
358
  "tinycloud",
@@ -390,9 +423,9 @@ var package_default = {
390
423
  dependencies: {
391
424
  "@modelcontextprotocol/node": "2.0.0-beta.4",
392
425
  "@modelcontextprotocol/server": "2.0.0-beta.4",
393
- "@tinycloud/node-sdk": "3.2.0-beta.1",
426
+ "@tinycloud/node-sdk": "3.2.0-beta.9",
394
427
  "@tinycloud/node-sdk-wasm": "1.7.7",
395
- "@tinycloud/operations": "0.5.0-beta.1",
428
+ "@tinycloud/operations": "0.5.0-beta.10",
396
429
  jose: "6.1.3"
397
430
  },
398
431
  devDependencies: {
@@ -419,7 +452,7 @@ function createTinyCloudMcpServer(selection) {
419
452
  { name: MCP_SERVER_NAME, version: MCP_VERSION },
420
453
  { jsonSchemaValidator: validator }
421
454
  );
422
- registerTinyCloudTools(server, selection, validator);
455
+ registerTinyCloudTools(server, { ...selection, surface: "local" }, validator);
423
456
  return server;
424
457
  }
425
458
  function serveTinyCloudMcp(selection) {
@@ -535,6 +568,7 @@ var import_server5 = require("@modelcontextprotocol/server");
535
568
 
536
569
  // src/oauth.ts
537
570
  var import_server4 = require("@modelcontextprotocol/server");
571
+ var import_node_sdk = require("@tinycloud/node-sdk");
538
572
  var import_jose = require("jose");
539
573
  var OWNER_DIDS_CLAIM = "https://tinycloud.xyz/owner_dids";
540
574
  function createOpenKeyTokenVerifier(config) {
@@ -571,11 +605,11 @@ function authInfo(token, payload, config) {
571
605
  );
572
606
  }
573
607
  const clientId = firstString(payload.azp, payload.client_id) ?? "unknown";
574
- const ownerDids = Array.isArray(payload[OWNER_DIDS_CLAIM]) ? payload[OWNER_DIDS_CLAIM].filter((value) => typeof value === "string") : [];
575
- if (ownerDids.length === 0) {
608
+ const ownerDids = canonicalOwnerDids(payload[OWNER_DIDS_CLAIM]);
609
+ if (ownerDids === void 0) {
576
610
  throw new import_server4.OAuthError(
577
611
  import_server4.OAuthErrorCode.InvalidToken,
578
- "The OpenKey access token has no TinyCloud owner identity."
612
+ "The OpenKey access token has no valid TinyCloud owner identity."
579
613
  );
580
614
  }
581
615
  return {
@@ -590,6 +624,21 @@ function authInfo(token, payload, config) {
590
624
  }
591
625
  };
592
626
  }
627
+ var OWNER_DID = /^did:pkh:eip155:([1-9][0-9]{0,15}):(0x[0-9a-fA-F]{40})$/;
628
+ function canonicalOwnerDids(claim) {
629
+ if (!Array.isArray(claim) || claim.length === 0) return void 0;
630
+ const ownerDids = [];
631
+ for (const value of claim) {
632
+ const match = typeof value === "string" ? OWNER_DID.exec(value) : null;
633
+ if (match === null) return void 0;
634
+ try {
635
+ ownerDids.push((0, import_node_sdk.pkhDid)(match[2], Number(match[1])));
636
+ } catch {
637
+ return void 0;
638
+ }
639
+ }
640
+ return [...new Set(ownerDids)];
641
+ }
593
642
  function firstString(...values) {
594
643
  return values.find((value) => typeof value === "string" && value.length > 0);
595
644
  }
@@ -598,7 +647,7 @@ function firstString(...values) {
598
647
  var import_node_crypto = require("crypto");
599
648
  var import_promises = require("fs/promises");
600
649
  var import_node_path3 = require("path");
601
- var import_node_sdk = require("@tinycloud/node-sdk");
650
+ var import_node_sdk2 = require("@tinycloud/node-sdk");
602
651
  var import_node_sdk_wasm = require("@tinycloud/node-sdk-wasm");
603
652
  var import_operations2 = require("@tinycloud/operations");
604
653
  var PROFILE = "agent";
@@ -712,8 +761,7 @@ var RemoteTenantStore = class {
712
761
  if (pending.ownerDids.length > 0 && !pending.ownerDids.some((did) => samePrincipal(node.did, did))) {
713
762
  throw new ApprovalOwnerMismatchError();
714
763
  }
715
- const granted = [...node.getVerifiedSessionCapabilities()];
716
- if (!permissionsCover(pending.permissions, granted)) {
764
+ if (!(0, import_node_sdk2.isCapabilitySubset)(pending.permissions, node.getVerifiedSessionCapabilities()).subset) {
717
765
  throw new Error("The approved delegation does not cover the requested account access.");
718
766
  }
719
767
  const profile = await readJson(this.#profilePath(tenant.stateRoot));
@@ -729,14 +777,25 @@ var RemoteTenantStore = class {
729
777
  }
730
778
  async #storeDelegation(tenant, pending, data) {
731
779
  if (pending.requestId === void 0) throw new Error("Authority request is missing.");
732
- const { node } = await this.#restoreApproval(tenant, data);
780
+ const { node, session } = await this.#restoreApproval(tenant, data);
733
781
  if (!pending.ownerDids.some((did) => samePrincipal(node.did, did))) throw new ApprovalOwnerMismatchError();
734
- const delegation = portableFromCallback(data, pending.permissions, this.#config.nodeHost);
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
+ });
735
792
  const result = await (0, import_operations2.invokeOperation)(
736
793
  "tinycloud.auth.import",
737
794
  1,
738
795
  { profile: PROFILE, stateRoot: tenant.stateRoot },
739
796
  {
797
+ kind: "tinycloud.auth.delegation",
798
+ version: 1,
740
799
  requestId: pending.requestId,
741
800
  delegationCid: delegation.cid,
742
801
  delegation
@@ -746,12 +805,14 @@ var RemoteTenantStore = class {
746
805
  }
747
806
  /**
748
807
  * Verify the OpenKey callback as a SIWE session for this tenant's key. The
749
- * WASM verifier binds the authorization bytes and CID to the signed SIWE, so
750
- * `node.did` is the wallet that signed exactly the delegation being stored.
808
+ * callback must carry the full signed proof (SIWE, signature, address and
809
+ * chain), so the WASM verifier always runs: it binds the authorization bytes
810
+ * and CID to the signed SIWE, and `node.did` is the wallet that signed
811
+ * exactly the delegation being stored.
751
812
  */
752
813
  async #restoreApproval(tenant, data) {
753
814
  const session = sessionFromCallback(data, tenant.jwk);
754
- const node = new import_node_sdk.TinyCloudNode({ host: this.#config.nodeHost });
815
+ const node = new import_node_sdk2.TinyCloudNode({ host: this.#config.nodeHost });
755
816
  await node.restoreSession(session);
756
817
  if (!samePrincipal(node.sessionDid, tenant.sessionDid)) {
757
818
  throw new Error("The approved delegation targets a different session.");
@@ -772,7 +833,7 @@ var RemoteTenantStore = class {
772
833
  const raw = await readJson(this.#sessionPath(tenant.stateRoot));
773
834
  if (raw === null) return false;
774
835
  try {
775
- const node = new import_node_sdk.TinyCloudNode({ host: this.#config.nodeHost });
836
+ const node = new import_node_sdk2.TinyCloudNode({ host: this.#config.nodeHost });
776
837
  await node.restoreSession(raw);
777
838
  return samePrincipal(node.sessionDid, tenant.sessionDid) && ownerDids.some((did) => samePrincipal(node.did, did));
778
839
  } catch {
@@ -927,7 +988,7 @@ function ensureWasm() {
927
988
  }
928
989
  function sessionFromCallback(data, privateJwk) {
929
990
  const delegationHeader = data.delegationHeader;
930
- if (!isRecord(delegationHeader) || typeof delegationHeader.Authorization !== "string" || typeof data.delegationCid !== "string" || typeof data.spaceId !== "string" || typeof data.verificationMethod !== "string") {
991
+ if (!isRecord(delegationHeader) || typeof delegationHeader.Authorization !== "string" || typeof data.delegationCid !== "string" || typeof data.spaceId !== "string" || typeof data.verificationMethod !== "string" || typeof data.address !== "string" || typeof data.chainId !== "number" || typeof data.siwe !== "string" || typeof data.signature !== "string") {
931
992
  throw new Error("OpenKey returned an incomplete delegation.");
932
993
  }
933
994
  return {
@@ -936,63 +997,12 @@ function sessionFromCallback(data, privateJwk) {
936
997
  spaceId: data.spaceId,
937
998
  jwk: privateJwk,
938
999
  verificationMethod: data.verificationMethod,
939
- ...typeof data.address === "string" ? { address: data.address } : {},
940
- ...typeof data.chainId === "number" ? { chainId: data.chainId } : {},
941
- ...typeof data.siwe === "string" ? { siwe: data.siwe } : {},
942
- ...typeof data.signature === "string" ? { signature: data.signature } : {}
1000
+ address: data.address,
1001
+ chainId: data.chainId,
1002
+ siwe: data.siwe,
1003
+ signature: data.signature
943
1004
  };
944
1005
  }
945
- function portableFromCallback(data, permissions, host) {
946
- const primary = permissions.find((permission) => permission.service !== "tinycloud.encryption") ?? permissions[0];
947
- if (primary === void 0) throw new Error("The authority request has no permissions.");
948
- const session = sessionFromCallback(data, {});
949
- const expiry = delegationExpiry(data);
950
- return {
951
- cid: session.delegationCid,
952
- delegationHeader: session.delegationHeader,
953
- spaceId: session.spaceId,
954
- path: primary.path,
955
- actions: [...primary.actions],
956
- resources: permissions.map((permission) => ({
957
- service: permission.service.startsWith("tinycloud.") ? permission.service.slice("tinycloud.".length) : permission.service,
958
- space: permission.service === "tinycloud.encryption" ? requiredPermissionSpace(permission) : session.spaceId,
959
- path: permission.path,
960
- actions: [...permission.actions]
961
- })),
962
- expiry,
963
- delegateDID: session.verificationMethod,
964
- ownerAddress: typeof data.address === "string" ? data.address : "",
965
- chainId: typeof data.chainId === "number" ? data.chainId : 1,
966
- host
967
- };
968
- }
969
- function delegationExpiry(data) {
970
- for (const key of ["expiry", "expiresAt", "expirationTime"]) {
971
- const parsed = parseDate(data[key]);
972
- if (parsed !== void 0) return parsed;
973
- }
974
- if (typeof data.siwe === "string") {
975
- const match = data.siwe.match(/^Expiration Time:\s*(.+)$/im);
976
- const parsed = parseDate(match?.[1]?.trim());
977
- if (parsed !== void 0) return parsed;
978
- }
979
- throw new Error("OpenKey did not return a delegation expiry.");
980
- }
981
- function permissionsCover(requested, granted) {
982
- return requested.every((request) => granted.some(
983
- (grant) => normalizeService(grant.service) === normalizeService(request.service) && typeof grant.space === "string" && typeof request.space === "string" && sameSpace(grant.space, request.space) && grant.path === request.path && request.actions.every((action) => grant.actions.includes(action))
984
- ));
985
- }
986
- function sameSpace(granted, requested) {
987
- return granted === requested || granted.endsWith(`:${requested}`);
988
- }
989
- function normalizeService(service) {
990
- return service.startsWith("tinycloud.") ? service : `tinycloud.${service}`;
991
- }
992
- function requiredPermissionSpace(permission) {
993
- if (typeof permission.space !== "string") throw new Error("The authority request has no space.");
994
- return permission.space;
995
- }
996
1006
  function publicJwk(jwk) {
997
1007
  const copy = { ...jwk };
998
1008
  for (const field of ["d", "p", "q", "dp", "dq", "qi", "oth", "k"]) delete copy[field];
@@ -1001,7 +1011,7 @@ function publicJwk(jwk) {
1001
1011
  function samePrincipal(left, right) {
1002
1012
  if (typeof left !== "string") return false;
1003
1013
  try {
1004
- return (0, import_node_sdk.principalDidEquals)(left, right);
1014
+ return (0, import_node_sdk2.principalDidEquals)(left, right);
1005
1015
  } catch {
1006
1016
  return false;
1007
1017
  }
@@ -1015,11 +1025,6 @@ function isPermissionEntry(value) {
1015
1025
  function isRecord(value) {
1016
1026
  return typeof value === "object" && value !== null && !Array.isArray(value);
1017
1027
  }
1018
- function parseDate(value) {
1019
- if (typeof value !== "string" && typeof value !== "number") return void 0;
1020
- const date = new Date(typeof value === "number" && value < 1e10 ? value * 1e3 : value);
1021
- return Number.isNaN(date.getTime()) ? void 0 : date;
1022
- }
1023
1028
  async function secureDirectory(path) {
1024
1029
  await (0, import_promises.mkdir)(path, { recursive: true, mode: 448 });
1025
1030
  await (0, import_promises.chmod)(path, 448);
@@ -1109,6 +1114,7 @@ function createHostedMcpApp(config) {
1109
1114
  }
1110
1115
  );
1111
1116
  registerTinyCloudTools(server, {
1117
+ surface: "hosted",
1112
1118
  profile: "agent",
1113
1119
  explicitProfile: true,
1114
1120
  allowOwnerProfile: false,