@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.cjs CHANGED
@@ -32,6 +32,7 @@ var import_server4 = require("@modelcontextprotocol/server");
32
32
 
33
33
  // src/oauth.ts
34
34
  var import_server = require("@modelcontextprotocol/server");
35
+ var import_node_sdk = require("@tinycloud/node-sdk");
35
36
  var import_jose = require("jose");
36
37
  var OWNER_DIDS_CLAIM = "https://tinycloud.xyz/owner_dids";
37
38
  function createOpenKeyTokenVerifier(config) {
@@ -80,11 +81,11 @@ function authInfo(token, payload, config) {
80
81
  );
81
82
  }
82
83
  const clientId = firstString(payload.azp, payload.client_id) ?? "unknown";
83
- const ownerDids = Array.isArray(payload[OWNER_DIDS_CLAIM]) ? payload[OWNER_DIDS_CLAIM].filter((value) => typeof value === "string") : [];
84
- if (ownerDids.length === 0) {
84
+ const ownerDids = canonicalOwnerDids(payload[OWNER_DIDS_CLAIM]);
85
+ if (ownerDids === void 0) {
85
86
  throw new import_server.OAuthError(
86
87
  import_server.OAuthErrorCode.InvalidToken,
87
- "The OpenKey access token has no TinyCloud owner identity."
88
+ "The OpenKey access token has no valid TinyCloud owner identity."
88
89
  );
89
90
  }
90
91
  return {
@@ -99,6 +100,21 @@ function authInfo(token, payload, config) {
99
100
  }
100
101
  };
101
102
  }
103
+ var OWNER_DID = /^did:pkh:eip155:([1-9][0-9]{0,15}):(0x[0-9a-fA-F]{40})$/;
104
+ function canonicalOwnerDids(claim) {
105
+ if (!Array.isArray(claim) || claim.length === 0) return void 0;
106
+ const ownerDids = [];
107
+ for (const value of claim) {
108
+ const match = typeof value === "string" ? OWNER_DID.exec(value) : null;
109
+ if (match === null) return void 0;
110
+ try {
111
+ ownerDids.push((0, import_node_sdk.pkhDid)(match[2], Number(match[1])));
112
+ } catch {
113
+ return void 0;
114
+ }
115
+ }
116
+ return [...new Set(ownerDids)];
117
+ }
102
118
  function firstString(...values) {
103
119
  return values.find((value) => typeof value === "string" && value.length > 0);
104
120
  }
@@ -107,7 +123,7 @@ function firstString(...values) {
107
123
  var import_node_crypto = require("crypto");
108
124
  var import_promises = require("fs/promises");
109
125
  var import_node_path = require("path");
110
- var import_node_sdk = require("@tinycloud/node-sdk");
126
+ var import_node_sdk2 = require("@tinycloud/node-sdk");
111
127
  var import_node_sdk_wasm = require("@tinycloud/node-sdk-wasm");
112
128
  var import_operations = require("@tinycloud/operations");
113
129
  var PROFILE = "agent";
@@ -221,8 +237,7 @@ var RemoteTenantStore = class {
221
237
  if (pending.ownerDids.length > 0 && !pending.ownerDids.some((did) => samePrincipal(node.did, did))) {
222
238
  throw new ApprovalOwnerMismatchError();
223
239
  }
224
- const granted = [...node.getVerifiedSessionCapabilities()];
225
- if (!permissionsCover(pending.permissions, granted)) {
240
+ if (!(0, import_node_sdk2.isCapabilitySubset)(pending.permissions, node.getVerifiedSessionCapabilities()).subset) {
226
241
  throw new Error("The approved delegation does not cover the requested account access.");
227
242
  }
228
243
  const profile = await readJson(this.#profilePath(tenant.stateRoot));
@@ -238,14 +253,25 @@ var RemoteTenantStore = class {
238
253
  }
239
254
  async #storeDelegation(tenant, pending, data) {
240
255
  if (pending.requestId === void 0) throw new Error("Authority request is missing.");
241
- const { node } = await this.#restoreApproval(tenant, data);
256
+ const { node, session } = await this.#restoreApproval(tenant, data);
242
257
  if (!pending.ownerDids.some((did) => samePrincipal(node.did, did))) throw new ApprovalOwnerMismatchError();
243
- const delegation = portableFromCallback(data, pending.permissions, this.#config.nodeHost);
258
+ const delegation = (0, import_node_sdk2.siweRuntimeDelegation)(node, {
259
+ authorization: session.delegationHeader.Authorization,
260
+ cid: session.delegationCid,
261
+ spaceId: session.spaceId,
262
+ ownerAddress: session.address,
263
+ chainId: session.chainId,
264
+ siwe: session.siwe,
265
+ signature: session.signature,
266
+ host: this.#config.nodeHost
267
+ });
244
268
  const result = await (0, import_operations.invokeOperation)(
245
269
  "tinycloud.auth.import",
246
270
  1,
247
271
  { profile: PROFILE, stateRoot: tenant.stateRoot },
248
272
  {
273
+ kind: "tinycloud.auth.delegation",
274
+ version: 1,
249
275
  requestId: pending.requestId,
250
276
  delegationCid: delegation.cid,
251
277
  delegation
@@ -255,12 +281,14 @@ var RemoteTenantStore = class {
255
281
  }
256
282
  /**
257
283
  * Verify the OpenKey callback as a SIWE session for this tenant's key. The
258
- * WASM verifier binds the authorization bytes and CID to the signed SIWE, so
259
- * `node.did` is the wallet that signed exactly the delegation being stored.
284
+ * callback must carry the full signed proof (SIWE, signature, address and
285
+ * chain), so the WASM verifier always runs: it binds the authorization bytes
286
+ * and CID to the signed SIWE, and `node.did` is the wallet that signed
287
+ * exactly the delegation being stored.
260
288
  */
261
289
  async #restoreApproval(tenant, data) {
262
290
  const session = sessionFromCallback(data, tenant.jwk);
263
- const node = new import_node_sdk.TinyCloudNode({ host: this.#config.nodeHost });
291
+ const node = new import_node_sdk2.TinyCloudNode({ host: this.#config.nodeHost });
264
292
  await node.restoreSession(session);
265
293
  if (!samePrincipal(node.sessionDid, tenant.sessionDid)) {
266
294
  throw new Error("The approved delegation targets a different session.");
@@ -281,7 +309,7 @@ var RemoteTenantStore = class {
281
309
  const raw = await readJson(this.#sessionPath(tenant.stateRoot));
282
310
  if (raw === null) return false;
283
311
  try {
284
- const node = new import_node_sdk.TinyCloudNode({ host: this.#config.nodeHost });
312
+ const node = new import_node_sdk2.TinyCloudNode({ host: this.#config.nodeHost });
285
313
  await node.restoreSession(raw);
286
314
  return samePrincipal(node.sessionDid, tenant.sessionDid) && ownerDids.some((did) => samePrincipal(node.did, did));
287
315
  } catch {
@@ -436,7 +464,7 @@ function ensureWasm() {
436
464
  }
437
465
  function sessionFromCallback(data, privateJwk) {
438
466
  const delegationHeader = data.delegationHeader;
439
- if (!isRecord(delegationHeader) || typeof delegationHeader.Authorization !== "string" || typeof data.delegationCid !== "string" || typeof data.spaceId !== "string" || typeof data.verificationMethod !== "string") {
467
+ 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") {
440
468
  throw new Error("OpenKey returned an incomplete delegation.");
441
469
  }
442
470
  return {
@@ -445,63 +473,12 @@ function sessionFromCallback(data, privateJwk) {
445
473
  spaceId: data.spaceId,
446
474
  jwk: privateJwk,
447
475
  verificationMethod: data.verificationMethod,
448
- ...typeof data.address === "string" ? { address: data.address } : {},
449
- ...typeof data.chainId === "number" ? { chainId: data.chainId } : {},
450
- ...typeof data.siwe === "string" ? { siwe: data.siwe } : {},
451
- ...typeof data.signature === "string" ? { signature: data.signature } : {}
452
- };
453
- }
454
- function portableFromCallback(data, permissions, host) {
455
- const primary = permissions.find((permission) => permission.service !== "tinycloud.encryption") ?? permissions[0];
456
- if (primary === void 0) throw new Error("The authority request has no permissions.");
457
- const session = sessionFromCallback(data, {});
458
- const expiry = delegationExpiry(data);
459
- return {
460
- cid: session.delegationCid,
461
- delegationHeader: session.delegationHeader,
462
- spaceId: session.spaceId,
463
- path: primary.path,
464
- actions: [...primary.actions],
465
- resources: permissions.map((permission) => ({
466
- service: permission.service.startsWith("tinycloud.") ? permission.service.slice("tinycloud.".length) : permission.service,
467
- space: permission.service === "tinycloud.encryption" ? requiredPermissionSpace(permission) : session.spaceId,
468
- path: permission.path,
469
- actions: [...permission.actions]
470
- })),
471
- expiry,
472
- delegateDID: session.verificationMethod,
473
- ownerAddress: typeof data.address === "string" ? data.address : "",
474
- chainId: typeof data.chainId === "number" ? data.chainId : 1,
475
- host
476
+ address: data.address,
477
+ chainId: data.chainId,
478
+ siwe: data.siwe,
479
+ signature: data.signature
476
480
  };
477
481
  }
478
- function delegationExpiry(data) {
479
- for (const key of ["expiry", "expiresAt", "expirationTime"]) {
480
- const parsed = parseDate(data[key]);
481
- if (parsed !== void 0) return parsed;
482
- }
483
- if (typeof data.siwe === "string") {
484
- const match = data.siwe.match(/^Expiration Time:\s*(.+)$/im);
485
- const parsed = parseDate(match?.[1]?.trim());
486
- if (parsed !== void 0) return parsed;
487
- }
488
- throw new Error("OpenKey did not return a delegation expiry.");
489
- }
490
- function permissionsCover(requested, granted) {
491
- return requested.every((request) => granted.some(
492
- (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))
493
- ));
494
- }
495
- function sameSpace(granted, requested) {
496
- return granted === requested || granted.endsWith(`:${requested}`);
497
- }
498
- function normalizeService(service) {
499
- return service.startsWith("tinycloud.") ? service : `tinycloud.${service}`;
500
- }
501
- function requiredPermissionSpace(permission) {
502
- if (typeof permission.space !== "string") throw new Error("The authority request has no space.");
503
- return permission.space;
504
- }
505
482
  function publicJwk(jwk) {
506
483
  const copy = { ...jwk };
507
484
  for (const field of ["d", "p", "q", "dp", "dq", "qi", "oth", "k"]) delete copy[field];
@@ -510,7 +487,7 @@ function publicJwk(jwk) {
510
487
  function samePrincipal(left, right) {
511
488
  if (typeof left !== "string") return false;
512
489
  try {
513
- return (0, import_node_sdk.principalDidEquals)(left, right);
490
+ return (0, import_node_sdk2.principalDidEquals)(left, right);
514
491
  } catch {
515
492
  return false;
516
493
  }
@@ -524,11 +501,6 @@ function isPermissionEntry(value) {
524
501
  function isRecord(value) {
525
502
  return typeof value === "object" && value !== null && !Array.isArray(value);
526
503
  }
527
- function parseDate(value) {
528
- if (typeof value !== "string" && typeof value !== "number") return void 0;
529
- const date = new Date(typeof value === "number" && value < 1e10 ? value * 1e3 : value);
530
- return Number.isNaN(date.getTime()) ? void 0 : date;
531
- }
532
504
  async function secureDirectory(path) {
533
505
  await (0, import_promises.mkdir)(path, { recursive: true, mode: 448 });
534
506
  await (0, import_promises.chmod)(path, 448);
@@ -598,8 +570,8 @@ var import_ajv = require("@modelcontextprotocol/server/validators/ajv");
598
570
  var import_operations2 = require("@tinycloud/operations");
599
571
 
600
572
  // src/results.ts
601
- function toMcpToolResult(result) {
602
- const canonical = isCanonicalOperationResult(result) ? result : {
573
+ function toMcpToolResult(result, bindings = []) {
574
+ const canonical = isCanonicalOperationResult(result) ? withRecoveryTool(result, bindings) : {
603
575
  status: "error",
604
576
  operation: {},
605
577
  context: {},
@@ -610,6 +582,18 @@ function toMcpToolResult(result) {
610
582
  structuredContent: canonical
611
583
  };
612
584
  }
585
+ function withRecoveryTool(result, bindings) {
586
+ if (result.status !== "error") return result;
587
+ const error = result.error;
588
+ const recovery = error?.details?.recovery;
589
+ if (recovery?.action !== "complete_migration") return result;
590
+ const binding = bindings.find((candidate) => candidate.operationId === recovery.operationId);
591
+ if (binding === void 0) return result;
592
+ return {
593
+ ...result,
594
+ error: { ...error, details: { ...error?.details, recovery: { ...recovery, tool: binding.name } } }
595
+ };
596
+ }
613
597
  function isCanonicalOperationResult(value) {
614
598
  if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
615
599
  const status = value.status;
@@ -643,6 +627,10 @@ var packageRequire = (0, import_node_module.createRequire)(
643
627
  var generatedCatalog = packageRequire(
644
628
  "@tinycloud/operations/operations.json"
645
629
  );
630
+ var ALL_SURFACES = ["local", "hosted"];
631
+ function bindingSurfaces(binding) {
632
+ return binding.surfaces ?? ALL_SURFACES;
633
+ }
646
634
  var TOOL_BINDINGS = [
647
635
  {
648
636
  name: "tinycloud_status",
@@ -687,7 +675,10 @@ var TOOL_BINDINGS = [
687
675
  readOnlyHint: false,
688
676
  idempotentHint: true,
689
677
  destructiveHint: false,
690
- openWorldHint: false
678
+ openWorldHint: false,
679
+ // Hosted delegations must arrive through the owner/request-bound OpenKey callback,
680
+ // not a caller-supplied import artifact.
681
+ surfaces: ["local"]
691
682
  },
692
683
  {
693
684
  name: "tinycloud_account_spaces_list",
@@ -787,6 +778,18 @@ var TOOL_BINDINGS = [
787
778
  idempotentHint: true,
788
779
  destructiveHint: false,
789
780
  openWorldHint: true
781
+ },
782
+ {
783
+ name: "tinycloud_secrets_status",
784
+ operationId: "tinycloud.secrets.status",
785
+ operationVersion: 1,
786
+ readOnlyHint: true,
787
+ idempotentHint: true,
788
+ destructiveHint: false,
789
+ openWorldHint: true,
790
+ // Hosted has no persistent reauthorization path for a revoked session
791
+ // yet (TC-896), so status is local/stdio only.
792
+ surfaces: ["local"]
790
793
  }
791
794
  ];
792
795
  function createJsonSchemaValidator() {
@@ -802,6 +805,7 @@ function registerTinyCloudTools(server, selection, validator) {
802
805
  ])
803
806
  );
804
807
  for (const binding of TOOL_BINDINGS) {
808
+ if (!bindingSurfaces(binding).includes(selection.surface)) continue;
805
809
  const operation = definitions.get(`${binding.operationId}@${binding.operationVersion}`);
806
810
  if (operation === void 0) {
807
811
  throw new Error(`Generated operations catalog is missing ${binding.operationId}@${binding.operationVersion}.`);
@@ -837,7 +841,7 @@ function registerTinyCloudTools(server, selection, validator) {
837
841
  input
838
842
  );
839
843
  const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
840
- return toMcpToolResult(projected);
844
+ return toMcpToolResult(projected, TOOL_BINDINGS);
841
845
  }
842
846
  );
843
847
  }
@@ -846,7 +850,7 @@ function registerTinyCloudTools(server, selection, validator) {
846
850
  // package.json
847
851
  var package_default = {
848
852
  name: "@tinycloud/mcp",
849
- version: "0.4.1-beta.1",
853
+ version: "0.5.0-beta.10",
850
854
  description: "TinyCloud delegated operations MCP server",
851
855
  keywords: [
852
856
  "tinycloud",
@@ -917,9 +921,9 @@ var package_default = {
917
921
  dependencies: {
918
922
  "@modelcontextprotocol/node": "2.0.0-beta.4",
919
923
  "@modelcontextprotocol/server": "2.0.0-beta.4",
920
- "@tinycloud/node-sdk": "3.2.0-beta.1",
924
+ "@tinycloud/node-sdk": "3.2.0-beta.9",
921
925
  "@tinycloud/node-sdk-wasm": "1.7.7",
922
- "@tinycloud/operations": "0.5.0-beta.1",
926
+ "@tinycloud/operations": "0.5.0-beta.10",
923
927
  jose: "6.1.3"
924
928
  },
925
929
  devDependencies: {
@@ -973,6 +977,7 @@ function createHostedMcpApp(config) {
973
977
  }
974
978
  );
975
979
  registerTinyCloudTools(server, {
980
+ surface: "hosted",
976
981
  profile: "agent",
977
982
  explicitProfile: true,
978
983
  allowOwnerProfile: false,