@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.js CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  oauthMetadataResponse,
14
14
  requireBearerAuth
15
15
  } from "@modelcontextprotocol/server";
16
+ import { RUNTIME_DELEGATION_REJECTIONS } from "@tinycloud/node-sdk";
16
17
 
17
18
  // src/oauth.ts
18
19
  import {
@@ -111,7 +112,10 @@ import { createHmac, randomBytes, timingSafeEqual } from "crypto";
111
112
  import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
112
113
  import { dirname, join } from "path";
113
114
  import {
115
+ InvalidRestoredSessionError,
116
+ RuntimeDelegationRejectedError,
114
117
  TinyCloudNode,
118
+ UnsupportedSessionRestoreError,
115
119
  isCapabilitySubset,
116
120
  principalDidEquals,
117
121
  siweRuntimeDelegation
@@ -120,6 +124,7 @@ import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk
120
124
  import { invokeOperation } from "@tinycloud/operations";
121
125
  var PROFILE = "agent";
122
126
  var MAX_CALLBACK_BYTES = 256 * 1024;
127
+ var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
123
128
  var BOOTSTRAP_PERMISSIONS = [
124
129
  {
125
130
  service: "tinycloud.kv",
@@ -140,6 +145,26 @@ var ApprovalOwnerMismatchError = class extends Error {
140
145
  this.name = "ApprovalOwnerMismatchError";
141
146
  }
142
147
  };
148
+ var ApprovalStageError = class extends Error {
149
+ stage;
150
+ code;
151
+ reason;
152
+ constructor(stage, code, reason, cause) {
153
+ super(`The approval failed at ${stage} (${code}).`, { cause });
154
+ this.name = "ApprovalStageError";
155
+ this.stage = stage;
156
+ this.code = code;
157
+ this.reason = reason;
158
+ }
159
+ };
160
+ var ApprovalRequestError = class extends Error {
161
+ detail;
162
+ constructor(detail, message, cause) {
163
+ super(message, { cause });
164
+ this.name = "ApprovalRequestError";
165
+ this.detail = detail;
166
+ }
167
+ };
143
168
  var RemoteTenantStore = class {
144
169
  #config;
145
170
  #initializing = /* @__PURE__ */ new Map();
@@ -205,11 +230,20 @@ var RemoteTenantStore = class {
205
230
  }
206
231
  async completeApproval(state, request) {
207
232
  const length = Number(request.headers.get("content-length") ?? "0");
208
- if (Number.isFinite(length) && length > MAX_CALLBACK_BYTES) throw new Error("Callback body is too large.");
233
+ if (Number.isFinite(length) && length > MAX_CALLBACK_BYTES) {
234
+ throw new ApprovalRequestError("body_invalid", "Callback body is too large.");
235
+ }
209
236
  const text = await request.text();
210
- if (Buffer.byteLength(text) > MAX_CALLBACK_BYTES) throw new Error("Callback body is too large.");
211
- const data = JSON.parse(text);
212
- if (!isRecord(data)) throw new Error("Callback body is invalid.");
237
+ if (Buffer.byteLength(text) > MAX_CALLBACK_BYTES) {
238
+ throw new ApprovalRequestError("body_invalid", "Callback body is too large.");
239
+ }
240
+ let data;
241
+ try {
242
+ data = JSON.parse(text);
243
+ } catch (error) {
244
+ throw new ApprovalRequestError("body_invalid", "Callback body is invalid.", error);
245
+ }
246
+ if (!isRecord(data)) throw new ApprovalRequestError("body_invalid", "Callback body is invalid.");
213
247
  const pending = await this.#readPending(state, true);
214
248
  try {
215
249
  const tenant = await this.#readTenant(pending.tenantId);
@@ -247,16 +281,26 @@ var RemoteTenantStore = class {
247
281
  if (pending.requestId === void 0) throw new Error("Authority request is missing.");
248
282
  const { node, session } = await this.#restoreApproval(tenant, data);
249
283
  if (!pending.ownerDids.some((did) => samePrincipal(node.did, did))) throw new ApprovalOwnerMismatchError();
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
- });
284
+ let delegation;
285
+ try {
286
+ delegation = siweRuntimeDelegation(node, {
287
+ authorization: session.delegationHeader.Authorization,
288
+ cid: session.delegationCid,
289
+ spaceId: session.spaceId,
290
+ ownerAddress: session.address,
291
+ chainId: session.chainId,
292
+ siwe: session.siwe,
293
+ signature: session.signature,
294
+ host: this.#config.nodeHost
295
+ });
296
+ } catch (error) {
297
+ throw new ApprovalStageError(
298
+ "siwe_runtime_delegation",
299
+ "DELEGATION_REJECTED",
300
+ error instanceof RuntimeDelegationRejectedError ? error.reason : void 0,
301
+ error
302
+ );
303
+ }
260
304
  const result = await invokeOperation(
261
305
  "tinycloud.auth.import",
262
306
  1,
@@ -269,7 +313,15 @@ var RemoteTenantStore = class {
269
313
  delegation
270
314
  }
271
315
  );
272
- if (result.status !== "ok") throw new Error("TinyCloud rejected the approved delegation.");
316
+ if (result.status === "error") {
317
+ const reason = result.error.details?.reason;
318
+ throw new ApprovalStageError(
319
+ result.error.code === "DELEGATION_REJECTED" && typeof reason === "string" ? "activation" : "auth_import",
320
+ result.error.code,
321
+ typeof reason === "string" ? reason : void 0
322
+ );
323
+ }
324
+ if (result.status !== "ok") throw new ApprovalStageError("auth_import", "AUTHORITY_REQUIRED");
273
325
  }
274
326
  /**
275
327
  * Verify the OpenKey callback as a SIWE session for this tenant's key. The
@@ -279,11 +331,25 @@ var RemoteTenantStore = class {
279
331
  * exactly the delegation being stored.
280
332
  */
281
333
  async #restoreApproval(tenant, data) {
282
- const session = sessionFromCallback(data, tenant.jwk);
334
+ let session;
335
+ try {
336
+ session = sessionFromCallback(data, tenant.jwk);
337
+ } catch (error) {
338
+ throw new ApprovalStageError("restore_approval", "CALLBACK_INCOMPLETE", void 0, error);
339
+ }
283
340
  const node = new TinyCloudNode({ host: this.#config.nodeHost });
284
- await node.restoreSession(session);
341
+ try {
342
+ await node.restoreSession(session);
343
+ } catch (error) {
344
+ throw new ApprovalStageError(
345
+ "restore_approval",
346
+ error instanceof InvalidRestoredSessionError || error instanceof UnsupportedSessionRestoreError ? error.code : "SESSION_RESTORE_FAILED",
347
+ void 0,
348
+ error
349
+ );
350
+ }
285
351
  if (!samePrincipal(node.sessionDid, tenant.sessionDid)) {
286
- throw new Error("The approved delegation targets a different session.");
352
+ throw new ApprovalStageError("restore_approval", "SESSION_MISMATCH");
287
353
  }
288
354
  return { node, session };
289
355
  }
@@ -324,7 +390,8 @@ var RemoteTenantStore = class {
324
390
  sessionDid = didFromJwk(jwk);
325
391
  }
326
392
  const profilePath = this.#profilePath(stateRoot);
327
- if (!await fileExists(profilePath)) {
393
+ const profile = await readJson(profilePath);
394
+ if (profile === null) {
328
395
  await secureWrite(profilePath, {
329
396
  name: PROFILE,
330
397
  host: this.#config.nodeHost,
@@ -338,14 +405,20 @@ var RemoteTenantStore = class {
338
405
  authMethod: "openkey",
339
406
  openkeyHost: this.#config.openkeyHost
340
407
  });
408
+ } else if (typeof profile.host === "string" && profile.host.replace(/\/+$/, "") === LEGACY_DEFAULT_NODE_HOST && this.#config.nodeHost.replace(/\/+$/, "") !== LEGACY_DEFAULT_NODE_HOST) {
409
+ await secureWrite(profilePath, { ...profile, host: this.#config.nodeHost });
410
+ process.stderr.write(
411
+ `[tinycloud-mcp-http] tenant node host migrated: from=${LEGACY_DEFAULT_NODE_HOST} to=${new URL(this.#config.nodeHost).origin}
412
+ `
413
+ );
341
414
  }
342
415
  return { stateRoot, sessionDid, jwk };
343
416
  }
344
417
  async #readTenant(tenantId) {
345
- if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new Error("Approval state is invalid.");
418
+ if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
346
419
  const stateRoot = join(this.#config.stateDir, "tenants", tenantId);
347
420
  const jwk = await readJson(this.#keyPath(stateRoot));
348
- if (jwk === null) throw new Error("The TinyCloud delegate no longer exists.");
421
+ if (jwk === null) throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.");
349
422
  return { stateRoot, jwk, sessionDid: didFromJwk(jwk) };
350
423
  }
351
424
  async #createPending(subject, ownerDids, kind, permissions, requestId) {
@@ -375,26 +448,40 @@ var RemoteTenantStore = class {
375
448
  async #readPending(state, consume) {
376
449
  const [tenantId, nonce, signature] = state.split(".");
377
450
  if (tenantId === void 0 || nonce === void 0 || signature === void 0) {
378
- throw new Error("Approval state is invalid.");
451
+ throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
379
452
  }
380
453
  const expected = this.#signState(tenantId, nonce);
381
454
  const supplied = Buffer.from(signature, "base64url");
382
455
  const expectedBytes = Buffer.from(expected, "base64url");
383
456
  if (supplied.length !== expectedBytes.length || !timingSafeEqual(supplied, expectedBytes)) {
384
- throw new Error("Approval state is invalid.");
457
+ throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
385
458
  }
386
459
  const stateRoot = join(this.#config.stateDir, "tenants", tenantId);
387
460
  const path = this.#pendingPath(stateRoot, nonce);
388
461
  const claimed = `${path}.processing`;
389
- if (consume) await rename(path, claimed);
462
+ if (consume) {
463
+ try {
464
+ await rename(path, claimed);
465
+ } catch (error) {
466
+ if (!isRecord(error) || error.code !== "ENOENT") throw error;
467
+ const tenantExists = await readJson(this.#keyPath(stateRoot)).then(
468
+ (jwk) => jwk !== null,
469
+ () => true
470
+ );
471
+ if (!tenantExists) {
472
+ throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.", error);
473
+ }
474
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.", error);
475
+ }
476
+ }
390
477
  try {
391
478
  const pending = await readJson(consume ? claimed : path);
392
479
  if (pending === null || pending.version !== 1 || pending.tenantId !== tenantId || pending.nonce !== nonce) {
393
- throw new Error("Approval state is invalid or already used.");
480
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
394
481
  }
395
482
  if (new Date(pending.expiresAt).getTime() <= Date.now()) {
396
483
  await rm(consume ? claimed : path, { force: true });
397
- throw new Error("Approval state has expired.");
484
+ throw new ApprovalRequestError("state_expired", "Approval state has expired.");
398
485
  }
399
486
  return pending;
400
487
  } catch (error) {
@@ -517,9 +604,6 @@ async function readJson(path) {
517
604
  throw error;
518
605
  }
519
606
  }
520
- async function fileExists(path) {
521
- return await readJson(path) !== null;
522
- }
523
607
  async function reusablePending(directory, expected) {
524
608
  let names;
525
609
  try {
@@ -841,7 +925,7 @@ function registerTinyCloudTools(server, selection, validator) {
841
925
  // package.json
842
926
  var package_default = {
843
927
  name: "@tinycloud/mcp",
844
- version: "0.5.4-beta.0",
928
+ version: "0.5.4-beta.2",
845
929
  description: "TinyCloud delegated operations MCP server",
846
930
  keywords: [
847
931
  "tinycloud",
@@ -914,7 +998,7 @@ var package_default = {
914
998
  "@modelcontextprotocol/server": "2.0.0-beta.4",
915
999
  "@tinycloud/node-sdk": "3.2.4-beta.0",
916
1000
  "@tinycloud/node-sdk-wasm": "1.7.7",
917
- "@tinycloud/operations": "0.5.4-beta.0",
1001
+ "@tinycloud/operations": "0.5.4-beta.1",
918
1002
  jose: "6.1.3"
919
1003
  },
920
1004
  devDependencies: {
@@ -1017,8 +1101,23 @@ function createHostedMcpApp(config) {
1017
1101
  await store.completeApproval(requiredState(url), request);
1018
1102
  return callbackCors(json({ success: true }), config.openkeyHost);
1019
1103
  } catch (error) {
1104
+ if (error instanceof ApprovalOwnerMismatchError) {
1105
+ return callbackCors(json({ error: error.message, code: "approval_owner_mismatch" }, 403), config.openkeyHost);
1106
+ }
1107
+ const stage = error instanceof ApprovalStageError ? error.stage : "unknown";
1108
+ const code = error instanceof ApprovalStageError && /^[A-Z][A-Z0-9_]{0,63}$/.test(error.code) ? error.code : "UNKNOWN";
1109
+ const reason = !(error instanceof ApprovalStageError) || error.reason === void 0 ? void 0 : RUNTIME_DELEGATION_REJECTIONS.find((known) => known === error.reason) ?? "unknown";
1110
+ const detail = error instanceof ApprovalRequestError ? error.detail : "none";
1111
+ writeDiagnostic(
1112
+ `connect callback rejected: stage=${stage} code=${code}${reason === void 0 ? "" : ` reason=${reason}`} detail=${detail}`
1113
+ );
1020
1114
  return callbackCors(
1021
- error instanceof ApprovalOwnerMismatchError ? json({ error: error.message, code: "approval_owner_mismatch" }, 403) : json({ error: "TinyCloud could not accept this delegation." }, 400),
1115
+ json({
1116
+ error: stage,
1117
+ code,
1118
+ ...reason === void 0 ? {} : { reason },
1119
+ message: "TinyCloud could not accept this delegation."
1120
+ }, 400),
1022
1121
  config.openkeyHost
1023
1122
  );
1024
1123
  }
@@ -1093,7 +1192,8 @@ function canonicalResourceUrl(url) {
1093
1192
  }
1094
1193
  function requiredState(url) {
1095
1194
  const state = url.searchParams.get("state");
1096
- if (state === null || state.length > 512) throw new Error("Missing approval state.");
1195
+ if (state === null) throw new ApprovalRequestError("state_missing", "Missing approval state.");
1196
+ if (state.length > 512) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
1097
1197
  return state;
1098
1198
  }
1099
1199
  function validateOrigin(request, allowed) {