@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.js CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  oauthMetadataResponse,
9
9
  requireBearerAuth
10
10
  } from "@modelcontextprotocol/server";
11
+ import { RUNTIME_DELEGATION_REJECTIONS } from "@tinycloud/node-sdk";
11
12
 
12
13
  // src/oauth.ts
13
14
  import {
@@ -94,7 +95,10 @@ import { createHmac, randomBytes, timingSafeEqual } from "crypto";
94
95
  import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
95
96
  import { dirname, join } from "path";
96
97
  import {
98
+ InvalidRestoredSessionError,
99
+ RuntimeDelegationRejectedError,
97
100
  TinyCloudNode,
101
+ UnsupportedSessionRestoreError,
98
102
  isCapabilitySubset,
99
103
  principalDidEquals,
100
104
  siweRuntimeDelegation
@@ -103,6 +107,7 @@ import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk
103
107
  import { invokeOperation } from "@tinycloud/operations";
104
108
  var PROFILE = "agent";
105
109
  var MAX_CALLBACK_BYTES = 256 * 1024;
110
+ var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
106
111
  var BOOTSTRAP_PERMISSIONS = [
107
112
  {
108
113
  service: "tinycloud.kv",
@@ -123,6 +128,26 @@ var ApprovalOwnerMismatchError = class extends Error {
123
128
  this.name = "ApprovalOwnerMismatchError";
124
129
  }
125
130
  };
131
+ var ApprovalStageError = class extends Error {
132
+ stage;
133
+ code;
134
+ reason;
135
+ constructor(stage, code, reason, cause) {
136
+ super(`The approval failed at ${stage} (${code}).`, { cause });
137
+ this.name = "ApprovalStageError";
138
+ this.stage = stage;
139
+ this.code = code;
140
+ this.reason = reason;
141
+ }
142
+ };
143
+ var ApprovalRequestError = class extends Error {
144
+ detail;
145
+ constructor(detail, message, cause) {
146
+ super(message, { cause });
147
+ this.name = "ApprovalRequestError";
148
+ this.detail = detail;
149
+ }
150
+ };
126
151
  var RemoteTenantStore = class {
127
152
  #config;
128
153
  #initializing = /* @__PURE__ */ new Map();
@@ -188,11 +213,20 @@ var RemoteTenantStore = class {
188
213
  }
189
214
  async completeApproval(state, request) {
190
215
  const length = Number(request.headers.get("content-length") ?? "0");
191
- if (Number.isFinite(length) && length > MAX_CALLBACK_BYTES) throw new Error("Callback body is too large.");
216
+ if (Number.isFinite(length) && length > MAX_CALLBACK_BYTES) {
217
+ throw new ApprovalRequestError("body_invalid", "Callback body is too large.");
218
+ }
192
219
  const text = await request.text();
193
- if (Buffer.byteLength(text) > MAX_CALLBACK_BYTES) throw new Error("Callback body is too large.");
194
- const data = JSON.parse(text);
195
- if (!isRecord(data)) throw new Error("Callback body is invalid.");
220
+ if (Buffer.byteLength(text) > MAX_CALLBACK_BYTES) {
221
+ throw new ApprovalRequestError("body_invalid", "Callback body is too large.");
222
+ }
223
+ let data;
224
+ try {
225
+ data = JSON.parse(text);
226
+ } catch (error) {
227
+ throw new ApprovalRequestError("body_invalid", "Callback body is invalid.", error);
228
+ }
229
+ if (!isRecord(data)) throw new ApprovalRequestError("body_invalid", "Callback body is invalid.");
196
230
  const pending = await this.#readPending(state, true);
197
231
  try {
198
232
  const tenant = await this.#readTenant(pending.tenantId);
@@ -230,16 +264,26 @@ var RemoteTenantStore = class {
230
264
  if (pending.requestId === void 0) throw new Error("Authority request is missing.");
231
265
  const { node, session } = await this.#restoreApproval(tenant, data);
232
266
  if (!pending.ownerDids.some((did) => samePrincipal(node.did, did))) throw new ApprovalOwnerMismatchError();
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
- });
267
+ let delegation;
268
+ try {
269
+ delegation = siweRuntimeDelegation(node, {
270
+ authorization: session.delegationHeader.Authorization,
271
+ cid: session.delegationCid,
272
+ spaceId: session.spaceId,
273
+ ownerAddress: session.address,
274
+ chainId: session.chainId,
275
+ siwe: session.siwe,
276
+ signature: session.signature,
277
+ host: this.#config.nodeHost
278
+ });
279
+ } catch (error) {
280
+ throw new ApprovalStageError(
281
+ "siwe_runtime_delegation",
282
+ "DELEGATION_REJECTED",
283
+ error instanceof RuntimeDelegationRejectedError ? error.reason : void 0,
284
+ error
285
+ );
286
+ }
243
287
  const result = await invokeOperation(
244
288
  "tinycloud.auth.import",
245
289
  1,
@@ -252,7 +296,15 @@ var RemoteTenantStore = class {
252
296
  delegation
253
297
  }
254
298
  );
255
- if (result.status !== "ok") throw new Error("TinyCloud rejected the approved delegation.");
299
+ if (result.status === "error") {
300
+ const reason = result.error.details?.reason;
301
+ throw new ApprovalStageError(
302
+ result.error.code === "DELEGATION_REJECTED" && typeof reason === "string" ? "activation" : "auth_import",
303
+ result.error.code,
304
+ typeof reason === "string" ? reason : void 0
305
+ );
306
+ }
307
+ if (result.status !== "ok") throw new ApprovalStageError("auth_import", "AUTHORITY_REQUIRED");
256
308
  }
257
309
  /**
258
310
  * Verify the OpenKey callback as a SIWE session for this tenant's key. The
@@ -262,11 +314,25 @@ var RemoteTenantStore = class {
262
314
  * exactly the delegation being stored.
263
315
  */
264
316
  async #restoreApproval(tenant, data) {
265
- const session = sessionFromCallback(data, tenant.jwk);
317
+ let session;
318
+ try {
319
+ session = sessionFromCallback(data, tenant.jwk);
320
+ } catch (error) {
321
+ throw new ApprovalStageError("restore_approval", "CALLBACK_INCOMPLETE", void 0, error);
322
+ }
266
323
  const node = new TinyCloudNode({ host: this.#config.nodeHost });
267
- await node.restoreSession(session);
324
+ try {
325
+ await node.restoreSession(session);
326
+ } catch (error) {
327
+ throw new ApprovalStageError(
328
+ "restore_approval",
329
+ error instanceof InvalidRestoredSessionError || error instanceof UnsupportedSessionRestoreError ? error.code : "SESSION_RESTORE_FAILED",
330
+ void 0,
331
+ error
332
+ );
333
+ }
268
334
  if (!samePrincipal(node.sessionDid, tenant.sessionDid)) {
269
- throw new Error("The approved delegation targets a different session.");
335
+ throw new ApprovalStageError("restore_approval", "SESSION_MISMATCH");
270
336
  }
271
337
  return { node, session };
272
338
  }
@@ -307,7 +373,8 @@ var RemoteTenantStore = class {
307
373
  sessionDid = didFromJwk(jwk);
308
374
  }
309
375
  const profilePath = this.#profilePath(stateRoot);
310
- if (!await fileExists(profilePath)) {
376
+ const profile = await readJson(profilePath);
377
+ if (profile === null) {
311
378
  await secureWrite(profilePath, {
312
379
  name: PROFILE,
313
380
  host: this.#config.nodeHost,
@@ -321,14 +388,20 @@ var RemoteTenantStore = class {
321
388
  authMethod: "openkey",
322
389
  openkeyHost: this.#config.openkeyHost
323
390
  });
391
+ } else if (typeof profile.host === "string" && profile.host.replace(/\/+$/, "") === LEGACY_DEFAULT_NODE_HOST && this.#config.nodeHost.replace(/\/+$/, "") !== LEGACY_DEFAULT_NODE_HOST) {
392
+ await secureWrite(profilePath, { ...profile, host: this.#config.nodeHost });
393
+ process.stderr.write(
394
+ `[tinycloud-mcp-http] tenant node host migrated: from=${LEGACY_DEFAULT_NODE_HOST} to=${new URL(this.#config.nodeHost).origin}
395
+ `
396
+ );
324
397
  }
325
398
  return { stateRoot, sessionDid, jwk };
326
399
  }
327
400
  async #readTenant(tenantId) {
328
- if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new Error("Approval state is invalid.");
401
+ if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
329
402
  const stateRoot = join(this.#config.stateDir, "tenants", tenantId);
330
403
  const jwk = await readJson(this.#keyPath(stateRoot));
331
- if (jwk === null) throw new Error("The TinyCloud delegate no longer exists.");
404
+ if (jwk === null) throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.");
332
405
  return { stateRoot, jwk, sessionDid: didFromJwk(jwk) };
333
406
  }
334
407
  async #createPending(subject, ownerDids, kind, permissions, requestId) {
@@ -358,26 +431,40 @@ var RemoteTenantStore = class {
358
431
  async #readPending(state, consume) {
359
432
  const [tenantId, nonce, signature] = state.split(".");
360
433
  if (tenantId === void 0 || nonce === void 0 || signature === void 0) {
361
- throw new Error("Approval state is invalid.");
434
+ throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
362
435
  }
363
436
  const expected = this.#signState(tenantId, nonce);
364
437
  const supplied = Buffer.from(signature, "base64url");
365
438
  const expectedBytes = Buffer.from(expected, "base64url");
366
439
  if (supplied.length !== expectedBytes.length || !timingSafeEqual(supplied, expectedBytes)) {
367
- throw new Error("Approval state is invalid.");
440
+ throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
368
441
  }
369
442
  const stateRoot = join(this.#config.stateDir, "tenants", tenantId);
370
443
  const path = this.#pendingPath(stateRoot, nonce);
371
444
  const claimed = `${path}.processing`;
372
- if (consume) await rename(path, claimed);
445
+ if (consume) {
446
+ try {
447
+ await rename(path, claimed);
448
+ } catch (error) {
449
+ if (!isRecord(error) || error.code !== "ENOENT") throw error;
450
+ const tenantExists = await readJson(this.#keyPath(stateRoot)).then(
451
+ (jwk) => jwk !== null,
452
+ () => true
453
+ );
454
+ if (!tenantExists) {
455
+ throw new ApprovalRequestError("tenant_missing", "The TinyCloud delegate no longer exists.", error);
456
+ }
457
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.", error);
458
+ }
459
+ }
373
460
  try {
374
461
  const pending = await readJson(consume ? claimed : path);
375
462
  if (pending === null || pending.version !== 1 || pending.tenantId !== tenantId || pending.nonce !== nonce) {
376
- throw new Error("Approval state is invalid or already used.");
463
+ throw new ApprovalRequestError("state_not_pending", "Approval state is invalid or already used.");
377
464
  }
378
465
  if (new Date(pending.expiresAt).getTime() <= Date.now()) {
379
466
  await rm(consume ? claimed : path, { force: true });
380
- throw new Error("Approval state has expired.");
467
+ throw new ApprovalRequestError("state_expired", "Approval state has expired.");
381
468
  }
382
469
  return pending;
383
470
  } catch (error) {
@@ -500,9 +587,6 @@ async function readJson(path) {
500
587
  throw error;
501
588
  }
502
589
  }
503
- async function fileExists(path) {
504
- return await readJson(path) !== null;
505
- }
506
590
  async function reusablePending(directory, expected) {
507
591
  let names;
508
592
  try {
@@ -824,7 +908,7 @@ function registerTinyCloudTools(server, selection, validator) {
824
908
  // package.json
825
909
  var package_default = {
826
910
  name: "@tinycloud/mcp",
827
- version: "0.5.4-beta.0",
911
+ version: "0.5.4-beta.2",
828
912
  description: "TinyCloud delegated operations MCP server",
829
913
  keywords: [
830
914
  "tinycloud",
@@ -897,7 +981,7 @@ var package_default = {
897
981
  "@modelcontextprotocol/server": "2.0.0-beta.4",
898
982
  "@tinycloud/node-sdk": "3.2.4-beta.0",
899
983
  "@tinycloud/node-sdk-wasm": "1.7.7",
900
- "@tinycloud/operations": "0.5.4-beta.0",
984
+ "@tinycloud/operations": "0.5.4-beta.1",
901
985
  jose: "6.1.3"
902
986
  },
903
987
  devDependencies: {
@@ -1000,8 +1084,23 @@ function createHostedMcpApp(config) {
1000
1084
  await store.completeApproval(requiredState(url), request);
1001
1085
  return callbackCors(json({ success: true }), config.openkeyHost);
1002
1086
  } catch (error) {
1087
+ if (error instanceof ApprovalOwnerMismatchError) {
1088
+ return callbackCors(json({ error: error.message, code: "approval_owner_mismatch" }, 403), config.openkeyHost);
1089
+ }
1090
+ const stage = error instanceof ApprovalStageError ? error.stage : "unknown";
1091
+ const code = error instanceof ApprovalStageError && /^[A-Z][A-Z0-9_]{0,63}$/.test(error.code) ? error.code : "UNKNOWN";
1092
+ const reason = !(error instanceof ApprovalStageError) || error.reason === void 0 ? void 0 : RUNTIME_DELEGATION_REJECTIONS.find((known) => known === error.reason) ?? "unknown";
1093
+ const detail = error instanceof ApprovalRequestError ? error.detail : "none";
1094
+ writeDiagnostic(
1095
+ `connect callback rejected: stage=${stage} code=${code}${reason === void 0 ? "" : ` reason=${reason}`} detail=${detail}`
1096
+ );
1003
1097
  return callbackCors(
1004
- error instanceof ApprovalOwnerMismatchError ? json({ error: error.message, code: "approval_owner_mismatch" }, 403) : json({ error: "TinyCloud could not accept this delegation." }, 400),
1098
+ json({
1099
+ error: stage,
1100
+ code,
1101
+ ...reason === void 0 ? {} : { reason },
1102
+ message: "TinyCloud could not accept this delegation."
1103
+ }, 400),
1005
1104
  config.openkeyHost
1006
1105
  );
1007
1106
  }
@@ -1076,7 +1175,8 @@ function canonicalResourceUrl(url) {
1076
1175
  }
1077
1176
  function requiredState(url) {
1078
1177
  const state = url.searchParams.get("state");
1079
- if (state === null || state.length > 512) throw new Error("Missing approval state.");
1178
+ if (state === null) throw new ApprovalRequestError("state_missing", "Missing approval state.");
1179
+ if (state.length > 512) throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
1080
1180
  return state;
1081
1181
  }
1082
1182
  function validateOrigin(request, allowed) {