@tinycloud/mcp 0.5.4-beta.0 → 0.5.4-beta.2

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
@@ -29,6 +29,7 @@ var import_node = require("@modelcontextprotocol/node");
29
29
 
30
30
  // src/http.ts
31
31
  var import_server4 = require("@modelcontextprotocol/server");
32
+ var import_node_sdk3 = require("@tinycloud/node-sdk");
32
33
 
33
34
  // src/oauth.ts
34
35
  var import_server = require("@modelcontextprotocol/server");
@@ -128,6 +129,7 @@ var import_node_sdk_wasm = require("@tinycloud/node-sdk-wasm");
128
129
  var import_operations = require("@tinycloud/operations");
129
130
  var PROFILE = "agent";
130
131
  var MAX_CALLBACK_BYTES = 256 * 1024;
132
+ var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
131
133
  var BOOTSTRAP_PERMISSIONS = [
132
134
  {
133
135
  service: "tinycloud.kv",
@@ -148,6 +150,26 @@ var ApprovalOwnerMismatchError = class extends Error {
148
150
  this.name = "ApprovalOwnerMismatchError";
149
151
  }
150
152
  };
153
+ var ApprovalStageError = class extends Error {
154
+ stage;
155
+ code;
156
+ reason;
157
+ constructor(stage, code, reason, cause) {
158
+ super(`The approval failed at ${stage} (${code}).`, { cause });
159
+ this.name = "ApprovalStageError";
160
+ this.stage = stage;
161
+ this.code = code;
162
+ this.reason = reason;
163
+ }
164
+ };
165
+ var ApprovalRequestError = class extends Error {
166
+ detail;
167
+ constructor(detail, message, cause) {
168
+ super(message, { cause });
169
+ this.name = "ApprovalRequestError";
170
+ this.detail = detail;
171
+ }
172
+ };
151
173
  var RemoteTenantStore = class {
152
174
  #config;
153
175
  #initializing = /* @__PURE__ */ new Map();
@@ -213,11 +235,20 @@ var RemoteTenantStore = class {
213
235
  }
214
236
  async completeApproval(state, request) {
215
237
  const length = Number(request.headers.get("content-length") ?? "0");
216
- if (Number.isFinite(length) && length > MAX_CALLBACK_BYTES) throw new Error("Callback body is too large.");
238
+ if (Number.isFinite(length) && length > MAX_CALLBACK_BYTES) {
239
+ throw new ApprovalRequestError("body_invalid", "Callback body is too large.");
240
+ }
217
241
  const text = await request.text();
218
- if (Buffer.byteLength(text) > MAX_CALLBACK_BYTES) throw new Error("Callback body is too large.");
219
- const data = JSON.parse(text);
220
- if (!isRecord(data)) throw new Error("Callback body is invalid.");
242
+ if (Buffer.byteLength(text) > MAX_CALLBACK_BYTES) {
243
+ throw new ApprovalRequestError("body_invalid", "Callback body is too large.");
244
+ }
245
+ let data;
246
+ try {
247
+ data = JSON.parse(text);
248
+ } catch (error) {
249
+ throw new ApprovalRequestError("body_invalid", "Callback body is invalid.", error);
250
+ }
251
+ if (!isRecord(data)) throw new ApprovalRequestError("body_invalid", "Callback body is invalid.");
221
252
  const pending = await this.#readPending(state, true);
222
253
  try {
223
254
  const tenant = await this.#readTenant(pending.tenantId);
@@ -255,16 +286,26 @@ var RemoteTenantStore = class {
255
286
  if (pending.requestId === void 0) throw new Error("Authority request is missing.");
256
287
  const { node, session } = await this.#restoreApproval(tenant, data);
257
288
  if (!pending.ownerDids.some((did) => samePrincipal(node.did, did))) throw new ApprovalOwnerMismatchError();
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
- });
289
+ let delegation;
290
+ try {
291
+ delegation = (0, import_node_sdk2.siweRuntimeDelegation)(node, {
292
+ authorization: session.delegationHeader.Authorization,
293
+ cid: session.delegationCid,
294
+ spaceId: session.spaceId,
295
+ ownerAddress: session.address,
296
+ chainId: session.chainId,
297
+ siwe: session.siwe,
298
+ signature: session.signature,
299
+ host: this.#config.nodeHost
300
+ });
301
+ } catch (error) {
302
+ throw new ApprovalStageError(
303
+ "siwe_runtime_delegation",
304
+ "DELEGATION_REJECTED",
305
+ error instanceof import_node_sdk2.RuntimeDelegationRejectedError ? error.reason : void 0,
306
+ error
307
+ );
308
+ }
268
309
  const result = await (0, import_operations.invokeOperation)(
269
310
  "tinycloud.auth.import",
270
311
  1,
@@ -277,7 +318,15 @@ var RemoteTenantStore = class {
277
318
  delegation
278
319
  }
279
320
  );
280
- if (result.status !== "ok") throw new Error("TinyCloud rejected the approved delegation.");
321
+ if (result.status === "error") {
322
+ const reason = result.error.details?.reason;
323
+ throw new ApprovalStageError(
324
+ result.error.code === "DELEGATION_REJECTED" && typeof reason === "string" ? "activation" : "auth_import",
325
+ result.error.code,
326
+ typeof reason === "string" ? reason : void 0
327
+ );
328
+ }
329
+ if (result.status !== "ok") throw new ApprovalStageError("auth_import", "AUTHORITY_REQUIRED");
281
330
  }
282
331
  /**
283
332
  * Verify the OpenKey callback as a SIWE session for this tenant's key. The
@@ -287,11 +336,25 @@ var RemoteTenantStore = class {
287
336
  * exactly the delegation being stored.
288
337
  */
289
338
  async #restoreApproval(tenant, data) {
290
- const session = sessionFromCallback(data, tenant.jwk);
339
+ let session;
340
+ try {
341
+ session = sessionFromCallback(data, tenant.jwk);
342
+ } catch (error) {
343
+ throw new ApprovalStageError("restore_approval", "CALLBACK_INCOMPLETE", void 0, error);
344
+ }
291
345
  const node = new import_node_sdk2.TinyCloudNode({ host: this.#config.nodeHost });
292
- await node.restoreSession(session);
346
+ try {
347
+ await node.restoreSession(session);
348
+ } catch (error) {
349
+ throw new ApprovalStageError(
350
+ "restore_approval",
351
+ error instanceof import_node_sdk2.InvalidRestoredSessionError || error instanceof import_node_sdk2.UnsupportedSessionRestoreError ? error.code : "SESSION_RESTORE_FAILED",
352
+ void 0,
353
+ error
354
+ );
355
+ }
293
356
  if (!samePrincipal(node.sessionDid, tenant.sessionDid)) {
294
- throw new Error("The approved delegation targets a different session.");
357
+ throw new ApprovalStageError("restore_approval", "SESSION_MISMATCH");
295
358
  }
296
359
  return { node, session };
297
360
  }
@@ -332,7 +395,8 @@ var RemoteTenantStore = class {
332
395
  sessionDid = didFromJwk(jwk);
333
396
  }
334
397
  const profilePath = this.#profilePath(stateRoot);
335
- if (!await fileExists(profilePath)) {
398
+ const profile = await readJson(profilePath);
399
+ if (profile === null) {
336
400
  await secureWrite(profilePath, {
337
401
  name: PROFILE,
338
402
  host: this.#config.nodeHost,
@@ -346,14 +410,20 @@ var RemoteTenantStore = class {
346
410
  authMethod: "openkey",
347
411
  openkeyHost: this.#config.openkeyHost
348
412
  });
413
+ } else if (typeof profile.host === "string" && profile.host.replace(/\/+$/, "") === LEGACY_DEFAULT_NODE_HOST && this.#config.nodeHost.replace(/\/+$/, "") !== LEGACY_DEFAULT_NODE_HOST) {
414
+ await secureWrite(profilePath, { ...profile, host: this.#config.nodeHost });
415
+ process.stderr.write(
416
+ `[tinycloud-mcp-http] tenant node host migrated: from=${LEGACY_DEFAULT_NODE_HOST} to=${new URL(this.#config.nodeHost).origin}
417
+ `
418
+ );
349
419
  }
350
420
  return { stateRoot, sessionDid, jwk };
351
421
  }
352
422
  async #readTenant(tenantId) {
353
- if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new Error("Approval state is invalid.");
423
+ if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
354
424
  const stateRoot = (0, import_node_path.join)(this.#config.stateDir, "tenants", tenantId);
355
425
  const jwk = await readJson(this.#keyPath(stateRoot));
356
- if (jwk === null) throw new Error("The TinyCloud delegate no longer exists.");
426
+ if (jwk === null) throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.");
357
427
  return { stateRoot, jwk, sessionDid: didFromJwk(jwk) };
358
428
  }
359
429
  async #createPending(subject, ownerDids, kind, permissions, requestId) {
@@ -383,26 +453,40 @@ var RemoteTenantStore = class {
383
453
  async #readPending(state, consume) {
384
454
  const [tenantId, nonce, signature] = state.split(".");
385
455
  if (tenantId === void 0 || nonce === void 0 || signature === void 0) {
386
- throw new Error("Approval state is invalid.");
456
+ throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
387
457
  }
388
458
  const expected = this.#signState(tenantId, nonce);
389
459
  const supplied = Buffer.from(signature, "base64url");
390
460
  const expectedBytes = Buffer.from(expected, "base64url");
391
461
  if (supplied.length !== expectedBytes.length || !(0, import_node_crypto.timingSafeEqual)(supplied, expectedBytes)) {
392
- throw new Error("Approval state is invalid.");
462
+ throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
393
463
  }
394
464
  const stateRoot = (0, import_node_path.join)(this.#config.stateDir, "tenants", tenantId);
395
465
  const path = this.#pendingPath(stateRoot, nonce);
396
466
  const claimed = `${path}.processing`;
397
- if (consume) await (0, import_promises.rename)(path, claimed);
467
+ if (consume) {
468
+ try {
469
+ await (0, import_promises.rename)(path, claimed);
470
+ } catch (error) {
471
+ if (!isRecord(error) || error.code !== "ENOENT") throw error;
472
+ const tenantExists = await readJson(this.#keyPath(stateRoot)).then(
473
+ (jwk) => jwk !== null,
474
+ () => true
475
+ );
476
+ if (!tenantExists) {
477
+ throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.", error);
478
+ }
479
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.", error);
480
+ }
481
+ }
398
482
  try {
399
483
  const pending = await readJson(consume ? claimed : path);
400
484
  if (pending === null || pending.version !== 1 || pending.tenantId !== tenantId || pending.nonce !== nonce) {
401
- throw new Error("Approval state is invalid or already used.");
485
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
402
486
  }
403
487
  if (new Date(pending.expiresAt).getTime() <= Date.now()) {
404
488
  await (0, import_promises.rm)(consume ? claimed : path, { force: true });
405
- throw new Error("Approval state has expired.");
489
+ throw new ApprovalRequestError("state_expired", "Approval state has expired.");
406
490
  }
407
491
  return pending;
408
492
  } catch (error) {
@@ -525,9 +609,6 @@ async function readJson(path) {
525
609
  throw error;
526
610
  }
527
611
  }
528
- async function fileExists(path) {
529
- return await readJson(path) !== null;
530
- }
531
612
  async function reusablePending(directory, expected) {
532
613
  let names;
533
614
  try {
@@ -850,7 +931,7 @@ function registerTinyCloudTools(server, selection, validator) {
850
931
  // package.json
851
932
  var package_default = {
852
933
  name: "@tinycloud/mcp",
853
- version: "0.5.4-beta.0",
934
+ version: "0.5.4-beta.2",
854
935
  description: "TinyCloud delegated operations MCP server",
855
936
  keywords: [
856
937
  "tinycloud",
@@ -923,7 +1004,7 @@ var package_default = {
923
1004
  "@modelcontextprotocol/server": "2.0.0-beta.4",
924
1005
  "@tinycloud/node-sdk": "3.2.4-beta.0",
925
1006
  "@tinycloud/node-sdk-wasm": "1.7.7",
926
- "@tinycloud/operations": "0.5.4-beta.0",
1007
+ "@tinycloud/operations": "0.5.4-beta.1",
927
1008
  jose: "6.1.3"
928
1009
  },
929
1010
  devDependencies: {
@@ -1026,8 +1107,23 @@ function createHostedMcpApp(config) {
1026
1107
  await store.completeApproval(requiredState(url), request);
1027
1108
  return callbackCors(json({ success: true }), config.openkeyHost);
1028
1109
  } catch (error) {
1110
+ if (error instanceof ApprovalOwnerMismatchError) {
1111
+ return callbackCors(json({ error: error.message, code: "approval_owner_mismatch" }, 403), config.openkeyHost);
1112
+ }
1113
+ const stage = error instanceof ApprovalStageError ? error.stage : "unknown";
1114
+ const code = error instanceof ApprovalStageError && /^[A-Z][A-Z0-9_]{0,63}$/.test(error.code) ? error.code : "UNKNOWN";
1115
+ const reason = !(error instanceof ApprovalStageError) || error.reason === void 0 ? void 0 : import_node_sdk3.RUNTIME_DELEGATION_REJECTIONS.find((known) => known === error.reason) ?? "unknown";
1116
+ const detail = error instanceof ApprovalRequestError ? error.detail : "none";
1117
+ writeDiagnostic(
1118
+ `connect callback rejected: stage=${stage} code=${code}${reason === void 0 ? "" : ` reason=${reason}`} detail=${detail}`
1119
+ );
1029
1120
  return callbackCors(
1030
- error instanceof ApprovalOwnerMismatchError ? json({ error: error.message, code: "approval_owner_mismatch" }, 403) : json({ error: "TinyCloud could not accept this delegation." }, 400),
1121
+ json({
1122
+ error: stage,
1123
+ code,
1124
+ ...reason === void 0 ? {} : { reason },
1125
+ message: "TinyCloud could not accept this delegation."
1126
+ }, 400),
1031
1127
  config.openkeyHost
1032
1128
  );
1033
1129
  }
@@ -1102,7 +1198,8 @@ function canonicalResourceUrl(url) {
1102
1198
  }
1103
1199
  function requiredState(url) {
1104
1200
  const state = url.searchParams.get("state");
1105
- if (state === null || state.length > 512) throw new Error("Missing approval state.");
1201
+ if (state === null) throw new ApprovalRequestError("state_missing", "Missing approval state.");
1202
+ if (state.length > 512) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
1106
1203
  return state;
1107
1204
  }
1108
1205
  function validateOrigin(request, allowed) {