@tinycloud/mcp 0.4.1-beta.0 → 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.js CHANGED
@@ -14,8 +14,8 @@ import { addFormats, Ajv, AjvJsonSchemaValidator } from "@modelcontextprotocol/s
14
14
  import { invokeOperation } from "@tinycloud/operations";
15
15
 
16
16
  // src/results.ts
17
- function toMcpToolResult(result) {
18
- const canonical = isCanonicalOperationResult(result) ? result : {
17
+ function toMcpToolResult(result, bindings = []) {
18
+ const canonical = isCanonicalOperationResult(result) ? withRecoveryTool(result, bindings) : {
19
19
  status: "error",
20
20
  operation: {},
21
21
  context: {},
@@ -26,6 +26,18 @@ function toMcpToolResult(result) {
26
26
  structuredContent: canonical
27
27
  };
28
28
  }
29
+ function withRecoveryTool(result, bindings) {
30
+ if (result.status !== "error") return result;
31
+ const error = result.error;
32
+ const recovery = error?.details?.recovery;
33
+ if (recovery?.action !== "complete_migration") return result;
34
+ const binding = bindings.find((candidate) => candidate.operationId === recovery.operationId);
35
+ if (binding === void 0) return result;
36
+ return {
37
+ ...result,
38
+ error: { ...error, details: { ...error?.details, recovery: { ...recovery, tool: binding.name } } }
39
+ };
40
+ }
29
41
  function canonicalMcpAdapterResult(result) {
30
42
  return toMcpToolResult(result).structuredContent;
31
43
  }
@@ -64,7 +76,8 @@ var TOOL_NAMES = [
64
76
  "tinycloud_sql_schema_inspect",
65
77
  "tinycloud_sql_query",
66
78
  "tinycloud_sql_execute",
67
- "tinycloud_secrets_get"
79
+ "tinycloud_secrets_get",
80
+ "tinycloud_secrets_status"
68
81
  ];
69
82
  function invocationTargetForMcp(selection) {
70
83
  return {
@@ -79,6 +92,10 @@ var packageRequire = createRequire(
79
92
  var generatedCatalog = packageRequire(
80
93
  "@tinycloud/operations/operations.json"
81
94
  );
95
+ var ALL_SURFACES = ["local", "hosted"];
96
+ function bindingSurfaces(binding) {
97
+ return binding.surfaces ?? ALL_SURFACES;
98
+ }
82
99
  var TOOL_BINDINGS = [
83
100
  {
84
101
  name: "tinycloud_status",
@@ -123,7 +140,10 @@ var TOOL_BINDINGS = [
123
140
  readOnlyHint: false,
124
141
  idempotentHint: true,
125
142
  destructiveHint: false,
126
- openWorldHint: false
143
+ openWorldHint: false,
144
+ // Hosted delegations must arrive through the owner/request-bound OpenKey callback,
145
+ // not a caller-supplied import artifact.
146
+ surfaces: ["local"]
127
147
  },
128
148
  {
129
149
  name: "tinycloud_account_spaces_list",
@@ -223,6 +243,18 @@ var TOOL_BINDINGS = [
223
243
  idempotentHint: true,
224
244
  destructiveHint: false,
225
245
  openWorldHint: true
246
+ },
247
+ {
248
+ name: "tinycloud_secrets_status",
249
+ operationId: "tinycloud.secrets.status",
250
+ operationVersion: 1,
251
+ readOnlyHint: true,
252
+ idempotentHint: true,
253
+ destructiveHint: false,
254
+ openWorldHint: true,
255
+ // Hosted has no persistent reauthorization path for a revoked session
256
+ // yet (TC-896), so status is local/stdio only.
257
+ surfaces: ["local"]
226
258
  }
227
259
  ];
228
260
  function createJsonSchemaValidator() {
@@ -238,6 +270,7 @@ function registerTinyCloudTools(server, selection, validator) {
238
270
  ])
239
271
  );
240
272
  for (const binding of TOOL_BINDINGS) {
273
+ if (!bindingSurfaces(binding).includes(selection.surface)) continue;
241
274
  const operation = definitions.get(`${binding.operationId}@${binding.operationVersion}`);
242
275
  if (operation === void 0) {
243
276
  throw new Error(`Generated operations catalog is missing ${binding.operationId}@${binding.operationVersion}.`);
@@ -273,7 +306,7 @@ function registerTinyCloudTools(server, selection, validator) {
273
306
  input
274
307
  );
275
308
  const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
276
- return toMcpToolResult(projected);
309
+ return toMcpToolResult(projected, TOOL_BINDINGS);
277
310
  }
278
311
  );
279
312
  }
@@ -282,7 +315,7 @@ function registerTinyCloudTools(server, selection, validator) {
282
315
  // package.json
283
316
  var package_default = {
284
317
  name: "@tinycloud/mcp",
285
- version: "0.4.1-beta.0",
318
+ version: "0.5.0-beta.10",
286
319
  description: "TinyCloud delegated operations MCP server",
287
320
  keywords: [
288
321
  "tinycloud",
@@ -353,9 +386,9 @@ var package_default = {
353
386
  dependencies: {
354
387
  "@modelcontextprotocol/node": "2.0.0-beta.4",
355
388
  "@modelcontextprotocol/server": "2.0.0-beta.4",
356
- "@tinycloud/node-sdk": "3.2.0-beta.0",
389
+ "@tinycloud/node-sdk": "3.2.0-beta.9",
357
390
  "@tinycloud/node-sdk-wasm": "1.7.7",
358
- "@tinycloud/operations": "0.5.0-beta.0",
391
+ "@tinycloud/operations": "0.5.0-beta.10",
359
392
  jose: "6.1.3"
360
393
  },
361
394
  devDependencies: {
@@ -382,7 +415,7 @@ function createTinyCloudMcpServer(selection) {
382
415
  { name: MCP_SERVER_NAME, version: MCP_VERSION },
383
416
  { jsonSchemaValidator: validator }
384
417
  );
385
- registerTinyCloudTools(server, selection, validator);
418
+ registerTinyCloudTools(server, { ...selection, surface: "local" }, validator);
386
419
  return server;
387
420
  }
388
421
  function serveTinyCloudMcp(selection) {
@@ -509,6 +542,7 @@ import {
509
542
  OAuthError,
510
543
  OAuthErrorCode
511
544
  } from "@modelcontextprotocol/server";
545
+ import { pkhDid } from "@tinycloud/node-sdk";
512
546
  import { createRemoteJWKSet, errors, jwtVerify } from "jose";
513
547
  var OWNER_DIDS_CLAIM = "https://tinycloud.xyz/owner_dids";
514
548
  function createOpenKeyTokenVerifier(config) {
@@ -545,11 +579,11 @@ function authInfo(token, payload, config) {
545
579
  );
546
580
  }
547
581
  const clientId = firstString(payload.azp, payload.client_id) ?? "unknown";
548
- const ownerDids = Array.isArray(payload[OWNER_DIDS_CLAIM]) ? payload[OWNER_DIDS_CLAIM].filter((value) => typeof value === "string") : [];
549
- if (ownerDids.length === 0) {
582
+ const ownerDids = canonicalOwnerDids(payload[OWNER_DIDS_CLAIM]);
583
+ if (ownerDids === void 0) {
550
584
  throw new OAuthError(
551
585
  OAuthErrorCode.InvalidToken,
552
- "The OpenKey access token has no TinyCloud owner identity."
586
+ "The OpenKey access token has no valid TinyCloud owner identity."
553
587
  );
554
588
  }
555
589
  return {
@@ -564,6 +598,21 @@ function authInfo(token, payload, config) {
564
598
  }
565
599
  };
566
600
  }
601
+ var OWNER_DID = /^did:pkh:eip155:([1-9][0-9]{0,15}):(0x[0-9a-fA-F]{40})$/;
602
+ function canonicalOwnerDids(claim) {
603
+ if (!Array.isArray(claim) || claim.length === 0) return void 0;
604
+ const ownerDids = [];
605
+ for (const value of claim) {
606
+ const match = typeof value === "string" ? OWNER_DID.exec(value) : null;
607
+ if (match === null) return void 0;
608
+ try {
609
+ ownerDids.push(pkhDid(match[2], Number(match[1])));
610
+ } catch {
611
+ return void 0;
612
+ }
613
+ }
614
+ return [...new Set(ownerDids)];
615
+ }
567
616
  function firstString(...values) {
568
617
  return values.find((value) => typeof value === "string" && value.length > 0);
569
618
  }
@@ -574,7 +623,9 @@ import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promi
574
623
  import { dirname, join as join2 } from "path";
575
624
  import {
576
625
  TinyCloudNode,
577
- principalDidEquals
626
+ isCapabilitySubset,
627
+ principalDidEquals,
628
+ siweRuntimeDelegation
578
629
  } from "@tinycloud/node-sdk";
579
630
  import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk-wasm";
580
631
  import { invokeOperation as invokeOperation2 } from "@tinycloud/operations";
@@ -689,8 +740,7 @@ var RemoteTenantStore = class {
689
740
  if (pending.ownerDids.length > 0 && !pending.ownerDids.some((did) => samePrincipal(node.did, did))) {
690
741
  throw new ApprovalOwnerMismatchError();
691
742
  }
692
- const granted = [...node.getVerifiedSessionCapabilities()];
693
- if (!permissionsCover(pending.permissions, granted)) {
743
+ if (!isCapabilitySubset(pending.permissions, node.getVerifiedSessionCapabilities()).subset) {
694
744
  throw new Error("The approved delegation does not cover the requested account access.");
695
745
  }
696
746
  const profile = await readJson(this.#profilePath(tenant.stateRoot));
@@ -706,14 +756,25 @@ var RemoteTenantStore = class {
706
756
  }
707
757
  async #storeDelegation(tenant, pending, data) {
708
758
  if (pending.requestId === void 0) throw new Error("Authority request is missing.");
709
- const { node } = await this.#restoreApproval(tenant, data);
759
+ const { node, session } = await this.#restoreApproval(tenant, data);
710
760
  if (!pending.ownerDids.some((did) => samePrincipal(node.did, did))) throw new ApprovalOwnerMismatchError();
711
- const delegation = portableFromCallback(data, pending.permissions, this.#config.nodeHost);
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
+ });
712
771
  const result = await invokeOperation2(
713
772
  "tinycloud.auth.import",
714
773
  1,
715
774
  { profile: PROFILE, stateRoot: tenant.stateRoot },
716
775
  {
776
+ kind: "tinycloud.auth.delegation",
777
+ version: 1,
717
778
  requestId: pending.requestId,
718
779
  delegationCid: delegation.cid,
719
780
  delegation
@@ -723,8 +784,10 @@ var RemoteTenantStore = class {
723
784
  }
724
785
  /**
725
786
  * Verify the OpenKey callback as a SIWE session for this tenant's key. The
726
- * WASM verifier binds the authorization bytes and CID to the signed SIWE, so
727
- * `node.did` is the wallet that signed exactly the delegation being stored.
787
+ * callback must carry the full signed proof (SIWE, signature, address and
788
+ * chain), so the WASM verifier always runs: it binds the authorization bytes
789
+ * and CID to the signed SIWE, and `node.did` is the wallet that signed
790
+ * exactly the delegation being stored.
728
791
  */
729
792
  async #restoreApproval(tenant, data) {
730
793
  const session = sessionFromCallback(data, tenant.jwk);
@@ -904,7 +967,7 @@ function ensureWasm() {
904
967
  }
905
968
  function sessionFromCallback(data, privateJwk) {
906
969
  const delegationHeader = data.delegationHeader;
907
- if (!isRecord(delegationHeader) || typeof delegationHeader.Authorization !== "string" || typeof data.delegationCid !== "string" || typeof data.spaceId !== "string" || typeof data.verificationMethod !== "string") {
970
+ 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") {
908
971
  throw new Error("OpenKey returned an incomplete delegation.");
909
972
  }
910
973
  return {
@@ -913,63 +976,12 @@ function sessionFromCallback(data, privateJwk) {
913
976
  spaceId: data.spaceId,
914
977
  jwk: privateJwk,
915
978
  verificationMethod: data.verificationMethod,
916
- ...typeof data.address === "string" ? { address: data.address } : {},
917
- ...typeof data.chainId === "number" ? { chainId: data.chainId } : {},
918
- ...typeof data.siwe === "string" ? { siwe: data.siwe } : {},
919
- ...typeof data.signature === "string" ? { signature: data.signature } : {}
979
+ address: data.address,
980
+ chainId: data.chainId,
981
+ siwe: data.siwe,
982
+ signature: data.signature
920
983
  };
921
984
  }
922
- function portableFromCallback(data, permissions, host) {
923
- const primary = permissions.find((permission) => permission.service !== "tinycloud.encryption") ?? permissions[0];
924
- if (primary === void 0) throw new Error("The authority request has no permissions.");
925
- const session = sessionFromCallback(data, {});
926
- const expiry = delegationExpiry(data);
927
- return {
928
- cid: session.delegationCid,
929
- delegationHeader: session.delegationHeader,
930
- spaceId: session.spaceId,
931
- path: primary.path,
932
- actions: [...primary.actions],
933
- resources: permissions.map((permission) => ({
934
- service: permission.service.startsWith("tinycloud.") ? permission.service.slice("tinycloud.".length) : permission.service,
935
- space: permission.service === "tinycloud.encryption" ? requiredPermissionSpace(permission) : session.spaceId,
936
- path: permission.path,
937
- actions: [...permission.actions]
938
- })),
939
- expiry,
940
- delegateDID: session.verificationMethod,
941
- ownerAddress: typeof data.address === "string" ? data.address : "",
942
- chainId: typeof data.chainId === "number" ? data.chainId : 1,
943
- host
944
- };
945
- }
946
- function delegationExpiry(data) {
947
- for (const key of ["expiry", "expiresAt", "expirationTime"]) {
948
- const parsed = parseDate(data[key]);
949
- if (parsed !== void 0) return parsed;
950
- }
951
- if (typeof data.siwe === "string") {
952
- const match = data.siwe.match(/^Expiration Time:\s*(.+)$/im);
953
- const parsed = parseDate(match?.[1]?.trim());
954
- if (parsed !== void 0) return parsed;
955
- }
956
- throw new Error("OpenKey did not return a delegation expiry.");
957
- }
958
- function permissionsCover(requested, granted) {
959
- return requested.every((request) => granted.some(
960
- (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))
961
- ));
962
- }
963
- function sameSpace(granted, requested) {
964
- return granted === requested || granted.endsWith(`:${requested}`);
965
- }
966
- function normalizeService(service) {
967
- return service.startsWith("tinycloud.") ? service : `tinycloud.${service}`;
968
- }
969
- function requiredPermissionSpace(permission) {
970
- if (typeof permission.space !== "string") throw new Error("The authority request has no space.");
971
- return permission.space;
972
- }
973
985
  function publicJwk(jwk) {
974
986
  const copy = { ...jwk };
975
987
  for (const field of ["d", "p", "q", "dp", "dq", "qi", "oth", "k"]) delete copy[field];
@@ -992,11 +1004,6 @@ function isPermissionEntry(value) {
992
1004
  function isRecord(value) {
993
1005
  return typeof value === "object" && value !== null && !Array.isArray(value);
994
1006
  }
995
- function parseDate(value) {
996
- if (typeof value !== "string" && typeof value !== "number") return void 0;
997
- const date = new Date(typeof value === "number" && value < 1e10 ? value * 1e3 : value);
998
- return Number.isNaN(date.getTime()) ? void 0 : date;
999
- }
1000
1007
  async function secureDirectory(path) {
1001
1008
  await mkdir(path, { recursive: true, mode: 448 });
1002
1009
  await chmod(path, 448);
@@ -1086,6 +1093,7 @@ function createHostedMcpApp(config) {
1086
1093
  }
1087
1094
  );
1088
1095
  registerTinyCloudTools(server, {
1096
+ surface: "hosted",
1089
1097
  profile: "agent",
1090
1098
  explicitProfile: true,
1091
1099
  allowOwnerProfile: false,