@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/http-cli.js CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  OAuthError,
20
20
  OAuthErrorCode
21
21
  } from "@modelcontextprotocol/server";
22
+ import { pkhDid } from "@tinycloud/node-sdk";
22
23
  import { createRemoteJWKSet, errors, jwtVerify } from "jose";
23
24
  var OWNER_DIDS_CLAIM = "https://tinycloud.xyz/owner_dids";
24
25
  function createOpenKeyTokenVerifier(config) {
@@ -67,11 +68,11 @@ function authInfo(token, payload, config) {
67
68
  );
68
69
  }
69
70
  const clientId = firstString(payload.azp, payload.client_id) ?? "unknown";
70
- const ownerDids = Array.isArray(payload[OWNER_DIDS_CLAIM]) ? payload[OWNER_DIDS_CLAIM].filter((value) => typeof value === "string") : [];
71
- if (ownerDids.length === 0) {
71
+ const ownerDids = canonicalOwnerDids(payload[OWNER_DIDS_CLAIM]);
72
+ if (ownerDids === void 0) {
72
73
  throw new OAuthError(
73
74
  OAuthErrorCode.InvalidToken,
74
- "The OpenKey access token has no TinyCloud owner identity."
75
+ "The OpenKey access token has no valid TinyCloud owner identity."
75
76
  );
76
77
  }
77
78
  return {
@@ -86,6 +87,21 @@ function authInfo(token, payload, config) {
86
87
  }
87
88
  };
88
89
  }
90
+ var OWNER_DID = /^did:pkh:eip155:([1-9][0-9]{0,15}):(0x[0-9a-fA-F]{40})$/;
91
+ function canonicalOwnerDids(claim) {
92
+ if (!Array.isArray(claim) || claim.length === 0) return void 0;
93
+ const ownerDids = [];
94
+ for (const value of claim) {
95
+ const match = typeof value === "string" ? OWNER_DID.exec(value) : null;
96
+ if (match === null) return void 0;
97
+ try {
98
+ ownerDids.push(pkhDid(match[2], Number(match[1])));
99
+ } catch {
100
+ return void 0;
101
+ }
102
+ }
103
+ return [...new Set(ownerDids)];
104
+ }
89
105
  function firstString(...values) {
90
106
  return values.find((value) => typeof value === "string" && value.length > 0);
91
107
  }
@@ -96,7 +112,9 @@ import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promi
96
112
  import { dirname, join } from "path";
97
113
  import {
98
114
  TinyCloudNode,
99
- principalDidEquals
115
+ isCapabilitySubset,
116
+ principalDidEquals,
117
+ siweRuntimeDelegation
100
118
  } from "@tinycloud/node-sdk";
101
119
  import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk-wasm";
102
120
  import { invokeOperation } from "@tinycloud/operations";
@@ -211,8 +229,7 @@ var RemoteTenantStore = class {
211
229
  if (pending.ownerDids.length > 0 && !pending.ownerDids.some((did) => samePrincipal(node.did, did))) {
212
230
  throw new ApprovalOwnerMismatchError();
213
231
  }
214
- const granted = [...node.getVerifiedSessionCapabilities()];
215
- if (!permissionsCover(pending.permissions, granted)) {
232
+ if (!isCapabilitySubset(pending.permissions, node.getVerifiedSessionCapabilities()).subset) {
216
233
  throw new Error("The approved delegation does not cover the requested account access.");
217
234
  }
218
235
  const profile = await readJson(this.#profilePath(tenant.stateRoot));
@@ -228,14 +245,25 @@ var RemoteTenantStore = class {
228
245
  }
229
246
  async #storeDelegation(tenant, pending, data) {
230
247
  if (pending.requestId === void 0) throw new Error("Authority request is missing.");
231
- const { node } = await this.#restoreApproval(tenant, data);
248
+ const { node, session } = await this.#restoreApproval(tenant, data);
232
249
  if (!pending.ownerDids.some((did) => samePrincipal(node.did, did))) throw new ApprovalOwnerMismatchError();
233
- const delegation = portableFromCallback(data, pending.permissions, this.#config.nodeHost);
250
+ const delegation = siweRuntimeDelegation(node, {
251
+ authorization: session.delegationHeader.Authorization,
252
+ cid: session.delegationCid,
253
+ spaceId: session.spaceId,
254
+ ownerAddress: session.address,
255
+ chainId: session.chainId,
256
+ siwe: session.siwe,
257
+ signature: session.signature,
258
+ host: this.#config.nodeHost
259
+ });
234
260
  const result = await invokeOperation(
235
261
  "tinycloud.auth.import",
236
262
  1,
237
263
  { profile: PROFILE, stateRoot: tenant.stateRoot },
238
264
  {
265
+ kind: "tinycloud.auth.delegation",
266
+ version: 1,
239
267
  requestId: pending.requestId,
240
268
  delegationCid: delegation.cid,
241
269
  delegation
@@ -245,8 +273,10 @@ var RemoteTenantStore = class {
245
273
  }
246
274
  /**
247
275
  * Verify the OpenKey callback as a SIWE session for this tenant's key. The
248
- * WASM verifier binds the authorization bytes and CID to the signed SIWE, so
249
- * `node.did` is the wallet that signed exactly the delegation being stored.
276
+ * callback must carry the full signed proof (SIWE, signature, address and
277
+ * chain), so the WASM verifier always runs: it binds the authorization bytes
278
+ * and CID to the signed SIWE, and `node.did` is the wallet that signed
279
+ * exactly the delegation being stored.
250
280
  */
251
281
  async #restoreApproval(tenant, data) {
252
282
  const session = sessionFromCallback(data, tenant.jwk);
@@ -426,7 +456,7 @@ function ensureWasm() {
426
456
  }
427
457
  function sessionFromCallback(data, privateJwk) {
428
458
  const delegationHeader = data.delegationHeader;
429
- if (!isRecord(delegationHeader) || typeof delegationHeader.Authorization !== "string" || typeof data.delegationCid !== "string" || typeof data.spaceId !== "string" || typeof data.verificationMethod !== "string") {
459
+ 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") {
430
460
  throw new Error("OpenKey returned an incomplete delegation.");
431
461
  }
432
462
  return {
@@ -435,63 +465,12 @@ function sessionFromCallback(data, privateJwk) {
435
465
  spaceId: data.spaceId,
436
466
  jwk: privateJwk,
437
467
  verificationMethod: data.verificationMethod,
438
- ...typeof data.address === "string" ? { address: data.address } : {},
439
- ...typeof data.chainId === "number" ? { chainId: data.chainId } : {},
440
- ...typeof data.siwe === "string" ? { siwe: data.siwe } : {},
441
- ...typeof data.signature === "string" ? { signature: data.signature } : {}
468
+ address: data.address,
469
+ chainId: data.chainId,
470
+ siwe: data.siwe,
471
+ signature: data.signature
442
472
  };
443
473
  }
444
- function portableFromCallback(data, permissions, host) {
445
- const primary = permissions.find((permission) => permission.service !== "tinycloud.encryption") ?? permissions[0];
446
- if (primary === void 0) throw new Error("The authority request has no permissions.");
447
- const session = sessionFromCallback(data, {});
448
- const expiry = delegationExpiry(data);
449
- return {
450
- cid: session.delegationCid,
451
- delegationHeader: session.delegationHeader,
452
- spaceId: session.spaceId,
453
- path: primary.path,
454
- actions: [...primary.actions],
455
- resources: permissions.map((permission) => ({
456
- service: permission.service.startsWith("tinycloud.") ? permission.service.slice("tinycloud.".length) : permission.service,
457
- space: permission.service === "tinycloud.encryption" ? requiredPermissionSpace(permission) : session.spaceId,
458
- path: permission.path,
459
- actions: [...permission.actions]
460
- })),
461
- expiry,
462
- delegateDID: session.verificationMethod,
463
- ownerAddress: typeof data.address === "string" ? data.address : "",
464
- chainId: typeof data.chainId === "number" ? data.chainId : 1,
465
- host
466
- };
467
- }
468
- function delegationExpiry(data) {
469
- for (const key of ["expiry", "expiresAt", "expirationTime"]) {
470
- const parsed = parseDate(data[key]);
471
- if (parsed !== void 0) return parsed;
472
- }
473
- if (typeof data.siwe === "string") {
474
- const match = data.siwe.match(/^Expiration Time:\s*(.+)$/im);
475
- const parsed = parseDate(match?.[1]?.trim());
476
- if (parsed !== void 0) return parsed;
477
- }
478
- throw new Error("OpenKey did not return a delegation expiry.");
479
- }
480
- function permissionsCover(requested, granted) {
481
- return requested.every((request) => granted.some(
482
- (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))
483
- ));
484
- }
485
- function sameSpace(granted, requested) {
486
- return granted === requested || granted.endsWith(`:${requested}`);
487
- }
488
- function normalizeService(service) {
489
- return service.startsWith("tinycloud.") ? service : `tinycloud.${service}`;
490
- }
491
- function requiredPermissionSpace(permission) {
492
- if (typeof permission.space !== "string") throw new Error("The authority request has no space.");
493
- return permission.space;
494
- }
495
474
  function publicJwk(jwk) {
496
475
  const copy = { ...jwk };
497
476
  for (const field of ["d", "p", "q", "dp", "dq", "qi", "oth", "k"]) delete copy[field];
@@ -514,11 +493,6 @@ function isPermissionEntry(value) {
514
493
  function isRecord(value) {
515
494
  return typeof value === "object" && value !== null && !Array.isArray(value);
516
495
  }
517
- function parseDate(value) {
518
- if (typeof value !== "string" && typeof value !== "number") return void 0;
519
- const date = new Date(typeof value === "number" && value < 1e10 ? value * 1e3 : value);
520
- return Number.isNaN(date.getTime()) ? void 0 : date;
521
- }
522
496
  async function secureDirectory(path) {
523
497
  await mkdir(path, { recursive: true, mode: 448 });
524
498
  await chmod(path, 448);
@@ -588,8 +562,8 @@ import { addFormats, Ajv, AjvJsonSchemaValidator } from "@modelcontextprotocol/s
588
562
  import { invokeOperation as invokeOperation2 } from "@tinycloud/operations";
589
563
 
590
564
  // src/results.ts
591
- function toMcpToolResult(result) {
592
- const canonical = isCanonicalOperationResult(result) ? result : {
565
+ function toMcpToolResult(result, bindings = []) {
566
+ const canonical = isCanonicalOperationResult(result) ? withRecoveryTool(result, bindings) : {
593
567
  status: "error",
594
568
  operation: {},
595
569
  context: {},
@@ -600,6 +574,18 @@ function toMcpToolResult(result) {
600
574
  structuredContent: canonical
601
575
  };
602
576
  }
577
+ function withRecoveryTool(result, bindings) {
578
+ if (result.status !== "error") return result;
579
+ const error = result.error;
580
+ const recovery = error?.details?.recovery;
581
+ if (recovery?.action !== "complete_migration") return result;
582
+ const binding = bindings.find((candidate) => candidate.operationId === recovery.operationId);
583
+ if (binding === void 0) return result;
584
+ return {
585
+ ...result,
586
+ error: { ...error, details: { ...error?.details, recovery: { ...recovery, tool: binding.name } } }
587
+ };
588
+ }
603
589
  function isCanonicalOperationResult(value) {
604
590
  if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
605
591
  const status = value.status;
@@ -632,6 +618,10 @@ var packageRequire = createRequire(
632
618
  var generatedCatalog = packageRequire(
633
619
  "@tinycloud/operations/operations.json"
634
620
  );
621
+ var ALL_SURFACES = ["local", "hosted"];
622
+ function bindingSurfaces(binding) {
623
+ return binding.surfaces ?? ALL_SURFACES;
624
+ }
635
625
  var TOOL_BINDINGS = [
636
626
  {
637
627
  name: "tinycloud_status",
@@ -676,7 +666,10 @@ var TOOL_BINDINGS = [
676
666
  readOnlyHint: false,
677
667
  idempotentHint: true,
678
668
  destructiveHint: false,
679
- openWorldHint: false
669
+ openWorldHint: false,
670
+ // Hosted delegations must arrive through the owner/request-bound OpenKey callback,
671
+ // not a caller-supplied import artifact.
672
+ surfaces: ["local"]
680
673
  },
681
674
  {
682
675
  name: "tinycloud_account_spaces_list",
@@ -776,6 +769,18 @@ var TOOL_BINDINGS = [
776
769
  idempotentHint: true,
777
770
  destructiveHint: false,
778
771
  openWorldHint: true
772
+ },
773
+ {
774
+ name: "tinycloud_secrets_status",
775
+ operationId: "tinycloud.secrets.status",
776
+ operationVersion: 1,
777
+ readOnlyHint: true,
778
+ idempotentHint: true,
779
+ destructiveHint: false,
780
+ openWorldHint: true,
781
+ // Hosted has no persistent reauthorization path for a revoked session
782
+ // yet (TC-896), so status is local/stdio only.
783
+ surfaces: ["local"]
779
784
  }
780
785
  ];
781
786
  function createJsonSchemaValidator() {
@@ -791,6 +796,7 @@ function registerTinyCloudTools(server, selection, validator) {
791
796
  ])
792
797
  );
793
798
  for (const binding of TOOL_BINDINGS) {
799
+ if (!bindingSurfaces(binding).includes(selection.surface)) continue;
794
800
  const operation = definitions.get(`${binding.operationId}@${binding.operationVersion}`);
795
801
  if (operation === void 0) {
796
802
  throw new Error(`Generated operations catalog is missing ${binding.operationId}@${binding.operationVersion}.`);
@@ -826,7 +832,7 @@ function registerTinyCloudTools(server, selection, validator) {
826
832
  input
827
833
  );
828
834
  const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
829
- return toMcpToolResult(projected);
835
+ return toMcpToolResult(projected, TOOL_BINDINGS);
830
836
  }
831
837
  );
832
838
  }
@@ -835,7 +841,7 @@ function registerTinyCloudTools(server, selection, validator) {
835
841
  // package.json
836
842
  var package_default = {
837
843
  name: "@tinycloud/mcp",
838
- version: "0.4.1-beta.1",
844
+ version: "0.5.0-beta.10",
839
845
  description: "TinyCloud delegated operations MCP server",
840
846
  keywords: [
841
847
  "tinycloud",
@@ -906,9 +912,9 @@ var package_default = {
906
912
  dependencies: {
907
913
  "@modelcontextprotocol/node": "2.0.0-beta.4",
908
914
  "@modelcontextprotocol/server": "2.0.0-beta.4",
909
- "@tinycloud/node-sdk": "3.2.0-beta.1",
915
+ "@tinycloud/node-sdk": "3.2.0-beta.9",
910
916
  "@tinycloud/node-sdk-wasm": "1.7.7",
911
- "@tinycloud/operations": "0.5.0-beta.1",
917
+ "@tinycloud/operations": "0.5.0-beta.10",
912
918
  jose: "6.1.3"
913
919
  },
914
920
  devDependencies: {
@@ -962,6 +968,7 @@ function createHostedMcpApp(config) {
962
968
  }
963
969
  );
964
970
  registerTinyCloudTools(server, {
971
+ surface: "hosted",
965
972
  profile: "agent",
966
973
  explicitProfile: true,
967
974
  allowOwnerProfile: false,