@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/http.js CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  OAuthError,
15
15
  OAuthErrorCode
16
16
  } from "@modelcontextprotocol/server";
17
+ import { pkhDid } from "@tinycloud/node-sdk";
17
18
  import { createRemoteJWKSet, errors, jwtVerify } from "jose";
18
19
  var OWNER_DIDS_CLAIM = "https://tinycloud.xyz/owner_dids";
19
20
  function createOpenKeyTokenVerifier(config) {
@@ -50,11 +51,11 @@ function authInfo(token, payload, config) {
50
51
  );
51
52
  }
52
53
  const clientId = firstString(payload.azp, payload.client_id) ?? "unknown";
53
- const ownerDids = Array.isArray(payload[OWNER_DIDS_CLAIM]) ? payload[OWNER_DIDS_CLAIM].filter((value) => typeof value === "string") : [];
54
- if (ownerDids.length === 0) {
54
+ const ownerDids = canonicalOwnerDids(payload[OWNER_DIDS_CLAIM]);
55
+ if (ownerDids === void 0) {
55
56
  throw new OAuthError(
56
57
  OAuthErrorCode.InvalidToken,
57
- "The OpenKey access token has no TinyCloud owner identity."
58
+ "The OpenKey access token has no valid TinyCloud owner identity."
58
59
  );
59
60
  }
60
61
  return {
@@ -69,6 +70,21 @@ function authInfo(token, payload, config) {
69
70
  }
70
71
  };
71
72
  }
73
+ var OWNER_DID = /^did:pkh:eip155:([1-9][0-9]{0,15}):(0x[0-9a-fA-F]{40})$/;
74
+ function canonicalOwnerDids(claim) {
75
+ if (!Array.isArray(claim) || claim.length === 0) return void 0;
76
+ const ownerDids = [];
77
+ for (const value of claim) {
78
+ const match = typeof value === "string" ? OWNER_DID.exec(value) : null;
79
+ if (match === null) return void 0;
80
+ try {
81
+ ownerDids.push(pkhDid(match[2], Number(match[1])));
82
+ } catch {
83
+ return void 0;
84
+ }
85
+ }
86
+ return [...new Set(ownerDids)];
87
+ }
72
88
  function firstString(...values) {
73
89
  return values.find((value) => typeof value === "string" && value.length > 0);
74
90
  }
@@ -79,7 +95,9 @@ import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promi
79
95
  import { dirname, join } from "path";
80
96
  import {
81
97
  TinyCloudNode,
82
- principalDidEquals
98
+ isCapabilitySubset,
99
+ principalDidEquals,
100
+ siweRuntimeDelegation
83
101
  } from "@tinycloud/node-sdk";
84
102
  import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk-wasm";
85
103
  import { invokeOperation } from "@tinycloud/operations";
@@ -194,8 +212,7 @@ var RemoteTenantStore = class {
194
212
  if (pending.ownerDids.length > 0 && !pending.ownerDids.some((did) => samePrincipal(node.did, did))) {
195
213
  throw new ApprovalOwnerMismatchError();
196
214
  }
197
- const granted = [...node.getVerifiedSessionCapabilities()];
198
- if (!permissionsCover(pending.permissions, granted)) {
215
+ if (!isCapabilitySubset(pending.permissions, node.getVerifiedSessionCapabilities()).subset) {
199
216
  throw new Error("The approved delegation does not cover the requested account access.");
200
217
  }
201
218
  const profile = await readJson(this.#profilePath(tenant.stateRoot));
@@ -211,14 +228,25 @@ var RemoteTenantStore = class {
211
228
  }
212
229
  async #storeDelegation(tenant, pending, data) {
213
230
  if (pending.requestId === void 0) throw new Error("Authority request is missing.");
214
- const { node } = await this.#restoreApproval(tenant, data);
231
+ const { node, session } = await this.#restoreApproval(tenant, data);
215
232
  if (!pending.ownerDids.some((did) => samePrincipal(node.did, did))) throw new ApprovalOwnerMismatchError();
216
- const delegation = portableFromCallback(data, pending.permissions, this.#config.nodeHost);
233
+ const delegation = siweRuntimeDelegation(node, {
234
+ authorization: session.delegationHeader.Authorization,
235
+ cid: session.delegationCid,
236
+ spaceId: session.spaceId,
237
+ ownerAddress: session.address,
238
+ chainId: session.chainId,
239
+ siwe: session.siwe,
240
+ signature: session.signature,
241
+ host: this.#config.nodeHost
242
+ });
217
243
  const result = await invokeOperation(
218
244
  "tinycloud.auth.import",
219
245
  1,
220
246
  { profile: PROFILE, stateRoot: tenant.stateRoot },
221
247
  {
248
+ kind: "tinycloud.auth.delegation",
249
+ version: 1,
222
250
  requestId: pending.requestId,
223
251
  delegationCid: delegation.cid,
224
252
  delegation
@@ -228,8 +256,10 @@ var RemoteTenantStore = class {
228
256
  }
229
257
  /**
230
258
  * Verify the OpenKey callback as a SIWE session for this tenant's key. The
231
- * WASM verifier binds the authorization bytes and CID to the signed SIWE, so
232
- * `node.did` is the wallet that signed exactly the delegation being stored.
259
+ * callback must carry the full signed proof (SIWE, signature, address and
260
+ * chain), so the WASM verifier always runs: it binds the authorization bytes
261
+ * and CID to the signed SIWE, and `node.did` is the wallet that signed
262
+ * exactly the delegation being stored.
233
263
  */
234
264
  async #restoreApproval(tenant, data) {
235
265
  const session = sessionFromCallback(data, tenant.jwk);
@@ -409,7 +439,7 @@ function ensureWasm() {
409
439
  }
410
440
  function sessionFromCallback(data, privateJwk) {
411
441
  const delegationHeader = data.delegationHeader;
412
- if (!isRecord(delegationHeader) || typeof delegationHeader.Authorization !== "string" || typeof data.delegationCid !== "string" || typeof data.spaceId !== "string" || typeof data.verificationMethod !== "string") {
442
+ 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") {
413
443
  throw new Error("OpenKey returned an incomplete delegation.");
414
444
  }
415
445
  return {
@@ -418,63 +448,12 @@ function sessionFromCallback(data, privateJwk) {
418
448
  spaceId: data.spaceId,
419
449
  jwk: privateJwk,
420
450
  verificationMethod: data.verificationMethod,
421
- ...typeof data.address === "string" ? { address: data.address } : {},
422
- ...typeof data.chainId === "number" ? { chainId: data.chainId } : {},
423
- ...typeof data.siwe === "string" ? { siwe: data.siwe } : {},
424
- ...typeof data.signature === "string" ? { signature: data.signature } : {}
451
+ address: data.address,
452
+ chainId: data.chainId,
453
+ siwe: data.siwe,
454
+ signature: data.signature
425
455
  };
426
456
  }
427
- function portableFromCallback(data, permissions, host) {
428
- const primary = permissions.find((permission) => permission.service !== "tinycloud.encryption") ?? permissions[0];
429
- if (primary === void 0) throw new Error("The authority request has no permissions.");
430
- const session = sessionFromCallback(data, {});
431
- const expiry = delegationExpiry(data);
432
- return {
433
- cid: session.delegationCid,
434
- delegationHeader: session.delegationHeader,
435
- spaceId: session.spaceId,
436
- path: primary.path,
437
- actions: [...primary.actions],
438
- resources: permissions.map((permission) => ({
439
- service: permission.service.startsWith("tinycloud.") ? permission.service.slice("tinycloud.".length) : permission.service,
440
- space: permission.service === "tinycloud.encryption" ? requiredPermissionSpace(permission) : session.spaceId,
441
- path: permission.path,
442
- actions: [...permission.actions]
443
- })),
444
- expiry,
445
- delegateDID: session.verificationMethod,
446
- ownerAddress: typeof data.address === "string" ? data.address : "",
447
- chainId: typeof data.chainId === "number" ? data.chainId : 1,
448
- host
449
- };
450
- }
451
- function delegationExpiry(data) {
452
- for (const key of ["expiry", "expiresAt", "expirationTime"]) {
453
- const parsed = parseDate(data[key]);
454
- if (parsed !== void 0) return parsed;
455
- }
456
- if (typeof data.siwe === "string") {
457
- const match = data.siwe.match(/^Expiration Time:\s*(.+)$/im);
458
- const parsed = parseDate(match?.[1]?.trim());
459
- if (parsed !== void 0) return parsed;
460
- }
461
- throw new Error("OpenKey did not return a delegation expiry.");
462
- }
463
- function permissionsCover(requested, granted) {
464
- return requested.every((request) => granted.some(
465
- (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))
466
- ));
467
- }
468
- function sameSpace(granted, requested) {
469
- return granted === requested || granted.endsWith(`:${requested}`);
470
- }
471
- function normalizeService(service) {
472
- return service.startsWith("tinycloud.") ? service : `tinycloud.${service}`;
473
- }
474
- function requiredPermissionSpace(permission) {
475
- if (typeof permission.space !== "string") throw new Error("The authority request has no space.");
476
- return permission.space;
477
- }
478
457
  function publicJwk(jwk) {
479
458
  const copy = { ...jwk };
480
459
  for (const field of ["d", "p", "q", "dp", "dq", "qi", "oth", "k"]) delete copy[field];
@@ -497,11 +476,6 @@ function isPermissionEntry(value) {
497
476
  function isRecord(value) {
498
477
  return typeof value === "object" && value !== null && !Array.isArray(value);
499
478
  }
500
- function parseDate(value) {
501
- if (typeof value !== "string" && typeof value !== "number") return void 0;
502
- const date = new Date(typeof value === "number" && value < 1e10 ? value * 1e3 : value);
503
- return Number.isNaN(date.getTime()) ? void 0 : date;
504
- }
505
479
  async function secureDirectory(path) {
506
480
  await mkdir(path, { recursive: true, mode: 448 });
507
481
  await chmod(path, 448);
@@ -571,8 +545,8 @@ import { addFormats, Ajv, AjvJsonSchemaValidator } from "@modelcontextprotocol/s
571
545
  import { invokeOperation as invokeOperation2 } from "@tinycloud/operations";
572
546
 
573
547
  // src/results.ts
574
- function toMcpToolResult(result) {
575
- const canonical = isCanonicalOperationResult(result) ? result : {
548
+ function toMcpToolResult(result, bindings = []) {
549
+ const canonical = isCanonicalOperationResult(result) ? withRecoveryTool(result, bindings) : {
576
550
  status: "error",
577
551
  operation: {},
578
552
  context: {},
@@ -583,6 +557,18 @@ function toMcpToolResult(result) {
583
557
  structuredContent: canonical
584
558
  };
585
559
  }
560
+ function withRecoveryTool(result, bindings) {
561
+ if (result.status !== "error") return result;
562
+ const error = result.error;
563
+ const recovery = error?.details?.recovery;
564
+ if (recovery?.action !== "complete_migration") return result;
565
+ const binding = bindings.find((candidate) => candidate.operationId === recovery.operationId);
566
+ if (binding === void 0) return result;
567
+ return {
568
+ ...result,
569
+ error: { ...error, details: { ...error?.details, recovery: { ...recovery, tool: binding.name } } }
570
+ };
571
+ }
586
572
  function isCanonicalOperationResult(value) {
587
573
  if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
588
574
  const status = value.status;
@@ -615,6 +601,10 @@ var packageRequire = createRequire(
615
601
  var generatedCatalog = packageRequire(
616
602
  "@tinycloud/operations/operations.json"
617
603
  );
604
+ var ALL_SURFACES = ["local", "hosted"];
605
+ function bindingSurfaces(binding) {
606
+ return binding.surfaces ?? ALL_SURFACES;
607
+ }
618
608
  var TOOL_BINDINGS = [
619
609
  {
620
610
  name: "tinycloud_status",
@@ -659,7 +649,10 @@ var TOOL_BINDINGS = [
659
649
  readOnlyHint: false,
660
650
  idempotentHint: true,
661
651
  destructiveHint: false,
662
- openWorldHint: false
652
+ openWorldHint: false,
653
+ // Hosted delegations must arrive through the owner/request-bound OpenKey callback,
654
+ // not a caller-supplied import artifact.
655
+ surfaces: ["local"]
663
656
  },
664
657
  {
665
658
  name: "tinycloud_account_spaces_list",
@@ -759,6 +752,18 @@ var TOOL_BINDINGS = [
759
752
  idempotentHint: true,
760
753
  destructiveHint: false,
761
754
  openWorldHint: true
755
+ },
756
+ {
757
+ name: "tinycloud_secrets_status",
758
+ operationId: "tinycloud.secrets.status",
759
+ operationVersion: 1,
760
+ readOnlyHint: true,
761
+ idempotentHint: true,
762
+ destructiveHint: false,
763
+ openWorldHint: true,
764
+ // Hosted has no persistent reauthorization path for a revoked session
765
+ // yet (TC-896), so status is local/stdio only.
766
+ surfaces: ["local"]
762
767
  }
763
768
  ];
764
769
  function createJsonSchemaValidator() {
@@ -774,6 +779,7 @@ function registerTinyCloudTools(server, selection, validator) {
774
779
  ])
775
780
  );
776
781
  for (const binding of TOOL_BINDINGS) {
782
+ if (!bindingSurfaces(binding).includes(selection.surface)) continue;
777
783
  const operation = definitions.get(`${binding.operationId}@${binding.operationVersion}`);
778
784
  if (operation === void 0) {
779
785
  throw new Error(`Generated operations catalog is missing ${binding.operationId}@${binding.operationVersion}.`);
@@ -809,7 +815,7 @@ function registerTinyCloudTools(server, selection, validator) {
809
815
  input
810
816
  );
811
817
  const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
812
- return toMcpToolResult(projected);
818
+ return toMcpToolResult(projected, TOOL_BINDINGS);
813
819
  }
814
820
  );
815
821
  }
@@ -818,7 +824,7 @@ function registerTinyCloudTools(server, selection, validator) {
818
824
  // package.json
819
825
  var package_default = {
820
826
  name: "@tinycloud/mcp",
821
- version: "0.4.1-beta.0",
827
+ version: "0.5.0-beta.10",
822
828
  description: "TinyCloud delegated operations MCP server",
823
829
  keywords: [
824
830
  "tinycloud",
@@ -889,9 +895,9 @@ var package_default = {
889
895
  dependencies: {
890
896
  "@modelcontextprotocol/node": "2.0.0-beta.4",
891
897
  "@modelcontextprotocol/server": "2.0.0-beta.4",
892
- "@tinycloud/node-sdk": "3.2.0-beta.0",
898
+ "@tinycloud/node-sdk": "3.2.0-beta.9",
893
899
  "@tinycloud/node-sdk-wasm": "1.7.7",
894
- "@tinycloud/operations": "0.5.0-beta.0",
900
+ "@tinycloud/operations": "0.5.0-beta.10",
895
901
  jose: "6.1.3"
896
902
  },
897
903
  devDependencies: {
@@ -945,6 +951,7 @@ function createHostedMcpApp(config) {
945
951
  }
946
952
  );
947
953
  registerTinyCloudTools(server, {
954
+ surface: "hosted",
948
955
  profile: "agent",
949
956
  explicitProfile: true,
950
957
  allowOwnerProfile: false,