@tinycloud/mcp 0.8.3-beta.2 → 0.8.3-beta.21

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
@@ -28,7 +28,9 @@ function createOpenKeyTokenVerifier(config) {
28
28
  issuer: config.issuer,
29
29
  audience: config.audience
30
30
  });
31
- return authInfo(token, payload, config);
31
+ const info = authInfo(token, payload, config);
32
+ if (config.tokenStatus !== void 0) await requireUsableStatus(config.tokenStatus, token);
33
+ return info;
32
34
  } catch (error) {
33
35
  if (error instanceof errors.JOSEError) {
34
36
  throw new OAuthError(OAuthErrorCode.InvalidToken, "The OpenKey access token is invalid.");
@@ -38,6 +40,16 @@ function createOpenKeyTokenVerifier(config) {
38
40
  }
39
41
  };
40
42
  }
43
+ async function requireUsableStatus(checker, token) {
44
+ const status = await checker.tokenStatus(token);
45
+ if (status === "active") return;
46
+ if (status === "inactive") {
47
+ throw new OAuthError(OAuthErrorCode.InvalidToken, "The OpenKey access token is no longer active.");
48
+ }
49
+ if (!checker.toleratesUnknown(token)) {
50
+ throw new OAuthError(OAuthErrorCode.ServerError, "The OpenKey access token status is unavailable.");
51
+ }
52
+ }
41
53
  function authInfo(token, payload, config) {
42
54
  if (typeof payload.sub !== "string" || payload.sub.length === 0) {
43
55
  throw new OAuthError(OAuthErrorCode.InvalidToken, "The OpenKey access token does not identify a subject.");
@@ -91,6 +103,150 @@ function firstString(...values) {
91
103
  return values.find((value) => typeof value === "string" && value.length > 0);
92
104
  }
93
105
 
106
+ // src/token-status.ts
107
+ import { createHash } from "crypto";
108
+ import { decodeJwt } from "jose";
109
+ var ACTIVE_CACHE_MS = 3e4;
110
+ var DEFAULT_TOKEN_LIFETIME_SECONDS = 300;
111
+ var DEFAULT_TIMEOUT_MS = 3e3;
112
+ var MAX_CACHED_TOKENS = 1e4;
113
+ function createTokenStatusChecker(config) {
114
+ const doFetch = config.fetch ?? fetch;
115
+ const clock = config.now ?? Date.now;
116
+ const log = config.log ?? ((message) => void process.stderr.write(`[tinycloud-mcp-http] ${message}
117
+ `));
118
+ const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
119
+ const inactive = /* @__PURE__ */ new Map();
120
+ const active = /* @__PURE__ */ new Map();
121
+ const inFlight = /* @__PURE__ */ new Map();
122
+ const confirmedActive = /* @__PURE__ */ new Map();
123
+ let credentialsRefused = false;
124
+ async function introspect(token) {
125
+ const headers = {
126
+ accept: "application/json",
127
+ "content-type": "application/x-www-form-urlencoded"
128
+ };
129
+ const body = new URLSearchParams({ token, token_type_hint: "access_token" });
130
+ if (config.authMethod === "basic") {
131
+ const encode = (value) => encodeURIComponent(value).replace(/%20/g, "+");
132
+ const credentials = `${encode(config.clientId)}:${encode(config.clientSecret)}`;
133
+ headers.authorization = `Basic ${Buffer.from(credentials).toString("base64")}`;
134
+ } else {
135
+ body.set("client_id", config.clientId);
136
+ body.set("client_secret", config.clientSecret);
137
+ }
138
+ try {
139
+ const response = await doFetch(config.endpoint, {
140
+ method: "POST",
141
+ headers,
142
+ body,
143
+ redirect: "error",
144
+ signal: AbortSignal.timeout(timeoutMs)
145
+ });
146
+ if (response.status === 401) return "credentials";
147
+ if (response.status !== 200) return "unknown";
148
+ const result = await response.json();
149
+ if (typeof result !== "object" || result === null || !("active" in result)) return "unknown";
150
+ if (result.active === false) return "inactive";
151
+ if (result.active !== true) return "unknown";
152
+ return answerMatchesToken(Object.fromEntries(Object.entries(result)), token, config.resourceUrl) ? "active" : "mismatch";
153
+ } catch {
154
+ return "unknown";
155
+ }
156
+ }
157
+ async function query(key, token) {
158
+ const outcome = await introspect(token);
159
+ if (outcome === "credentials") {
160
+ if (!credentialsRefused) {
161
+ log("OpenKey token introspection refused the MCP client credentials (401); token checks fail closed until it accepts them.");
162
+ }
163
+ credentialsRefused = true;
164
+ return "unknown";
165
+ }
166
+ if (outcome === "unknown") return outcome;
167
+ const now = clock();
168
+ prune(now);
169
+ if (outcome === "mismatch") {
170
+ inactive.set(key, tokenExpiryMs(token, now));
171
+ confirmedActive.delete(key);
172
+ return "inactive";
173
+ }
174
+ credentialsRefused = false;
175
+ if (outcome === "inactive") {
176
+ inactive.set(key, tokenExpiryMs(token, now));
177
+ confirmedActive.delete(key);
178
+ } else {
179
+ active.set(key, now + ACTIVE_CACHE_MS);
180
+ confirmedActive.delete(key);
181
+ confirmedActive.set(key, { at: now, expiresAt: tokenExpiryMs(token, now) });
182
+ }
183
+ return outcome;
184
+ }
185
+ function prune(now) {
186
+ for (const [key, until] of active) if (until <= now) active.delete(key);
187
+ for (const [key, until] of inactive) if (until <= now) inactive.delete(key);
188
+ for (const [key, entry] of confirmedActive) if (entry.expiresAt <= now) confirmedActive.delete(key);
189
+ for (const cache of [active, inactive, confirmedActive]) {
190
+ while (cache.size >= MAX_CACHED_TOKENS) {
191
+ const oldest = cache.keys().next().value;
192
+ if (oldest === void 0) break;
193
+ cache.delete(oldest);
194
+ }
195
+ }
196
+ }
197
+ return {
198
+ async tokenStatus(token) {
199
+ const key = createHash("sha256").update(token).digest("hex");
200
+ const now = clock();
201
+ const inactiveUntil = inactive.get(key);
202
+ if (inactiveUntil !== void 0 && inactiveUntil > now) return "inactive";
203
+ const activeUntil = active.get(key);
204
+ if (activeUntil !== void 0 && activeUntil > now) return "active";
205
+ const pending = inFlight.get(key);
206
+ if (pending !== void 0) return pending;
207
+ const asking = query(key, token).finally(() => inFlight.delete(key));
208
+ inFlight.set(key, asking);
209
+ return asking;
210
+ },
211
+ toleratesUnknown(token) {
212
+ if (credentialsRefused) return false;
213
+ const confirmed = confirmedActive.get(createHash("sha256").update(token).digest("hex"));
214
+ if (confirmed === void 0) return false;
215
+ const now = clock();
216
+ return confirmed.expiresAt > now && now - confirmed.at < tokenLifetimeSeconds(token) * 1e3;
217
+ }
218
+ };
219
+ }
220
+ function answerMatchesToken(answer, token, resourceUrl) {
221
+ const jwt = claims(token);
222
+ if (typeof jwt.sub === "string" && answer.sub !== jwt.sub) return false;
223
+ const jwtClient = [jwt.azp, jwt.client_id].find((value) => typeof value === "string" && value.length > 0);
224
+ if (jwtClient !== void 0 && answer.client_id !== jwtClient) return false;
225
+ if (jwt.aud === void 0) return true;
226
+ const answered = audienceList(answer.aud);
227
+ const signed = audienceList(jwt.aud);
228
+ return answered.length > 0 && answered.includes(resourceUrl) && answered.every((entry) => signed.includes(entry));
229
+ }
230
+ function audienceList(value) {
231
+ const list = typeof value === "string" ? [value] : value;
232
+ return Array.isArray(list) && list.every((entry) => typeof entry === "string") ? list : [];
233
+ }
234
+ function tokenExpiryMs(token, now) {
235
+ const exp = claims(token).exp;
236
+ return typeof exp === "number" ? exp * 1e3 : now + DEFAULT_TOKEN_LIFETIME_SECONDS * 1e3;
237
+ }
238
+ function tokenLifetimeSeconds(token) {
239
+ const { exp, iat } = claims(token);
240
+ return typeof exp === "number" && typeof iat === "number" && exp > iat ? exp - iat : DEFAULT_TOKEN_LIFETIME_SECONDS;
241
+ }
242
+ function claims(token) {
243
+ try {
244
+ return decodeJwt(token);
245
+ } catch {
246
+ return {};
247
+ }
248
+ }
249
+
94
250
  // src/logging.ts
95
251
  function logToolCall(tool, code, startedAt, replayedGrants) {
96
252
  process.stderr.write(`${JSON.stringify({
@@ -103,12 +259,15 @@ function logToolCall(tool, code, startedAt, replayedGrants) {
103
259
  }
104
260
 
105
261
  // src/remote-store.ts
106
- import { createHmac, randomBytes, timingSafeEqual } from "crypto";
262
+ import { createHmac, randomBytes, timingSafeEqual as timingSafeEqual2 } from "crypto";
107
263
  import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
108
264
  import { dirname, join } from "path";
109
265
  import {
266
+ activateValidatedRuntimeDelegation,
267
+ deserializeDelegation,
110
268
  InvalidRestoredSessionError,
111
269
  RuntimeDelegationRejectedError,
270
+ serializeDelegation,
112
271
  TinyCloudNode,
113
272
  UnsupportedSessionRestoreError,
114
273
  isCapabilitySubset,
@@ -117,11 +276,80 @@ import {
117
276
  signedCompactUcanExpiry
118
277
  } from "@tinycloud/node-sdk";
119
278
  import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk-wasm";
279
+ import { createOrReusePermissionRequest } from "@tinycloud/operations/artifacts";
280
+ import { withTinyCloudStateRoot } from "@tinycloud/operations/state";
120
281
  import { invokeOperation } from "@tinycloud/operations";
121
282
  import { nodeRevocationOf, operationRetryDescriptor } from "@tinycloud/operations/mcp-authority";
122
283
  import { tcAppsApprovalMessage, tcAppsApprovalReason } from "@tinycloud/operations/tc-apps";
284
+
285
+ // src/native-auth.ts
286
+ import { timingSafeEqual } from "crypto";
287
+ var NATIVE_SERVICE_CREDENTIAL_HEADER = "X-TinyCloud-Service-Credential";
288
+ var NATIVE_OWNER_DID_HEADER = "X-TinyCloud-Owner-DID";
289
+ var NATIVE_GRANT_MAX_LIFETIME_SECONDS = 30 * 24 * 60 * 60;
290
+ var NATIVE_GRANT_REQUIRED = "native_grant_required";
291
+ var NATIVE_GRANT_EXPIRED = "native_grant_expired";
292
+ var NATIVE_GRANT_REJECTED = "native_grant_rejected";
293
+ var NATIVE_SERVICE_UNAUTHORIZED = "native_service_unauthorized";
294
+ var NATIVE_SERVICE_UNAVAILABLE = "native_service_unavailable";
295
+ var NATIVE_GRANT_VALIDATION_UNAVAILABLE = "native_grant_validation_unavailable";
296
+ var MAX_NATIVE_GRANT_BODY_BYTES = 256 * 1024;
297
+ function serviceCredentialMatches(supplied, configured) {
298
+ if (supplied === null || configured === void 0) return false;
299
+ const suppliedBytes = Buffer.from(supplied, "utf8");
300
+ const configuredBytes = Buffer.from(configured, "utf8");
301
+ return suppliedBytes.byteLength === configuredBytes.byteLength && timingSafeEqual(suppliedBytes, configuredBytes);
302
+ }
303
+ var OWNER_APPLICATIONS_SPACE = /^tinycloud:pkh:eip155:([1-9][0-9]*):(0x[0-9a-f]{40}):applications$/i;
304
+ var SQL_PATH = "xyz.tinycloud.tinychat/connectors";
305
+ var KV_PREFIX = "xyz.tinycloud.tinychat/connectors/";
306
+ function canonicalOwnerDid(value) {
307
+ const match = /^did:pkh:eip155:([1-9][0-9]*):(0x[0-9a-f]{40})$/i.exec(value);
308
+ if (!match) return void 0;
309
+ const chainId = Number(match[1]);
310
+ if (!Number.isSafeInteger(chainId)) return void 0;
311
+ return `did:pkh:eip155:${chainId}:${match[2].toLowerCase()}`;
312
+ }
313
+ function nativeOwnerForConnectorGrant(permissions) {
314
+ if (permissions.length !== 2) return void 0;
315
+ const sqlEntries = permissions.filter((permission) => permission.service === "tinycloud.sql");
316
+ const kvEntries = permissions.filter((permission) => permission.service === "tinycloud.kv");
317
+ if (sqlEntries.length !== 1 || kvEntries.length !== 1) return void 0;
318
+ const sql = sqlEntries[0];
319
+ const kv = kvEntries[0];
320
+ if (typeof sql.space !== "string" || typeof kv.space !== "string" || sql.path !== SQL_PATH || kv.path !== KV_PREFIX) return void 0;
321
+ if (!actionsEqual(sql.actions, ["tinycloud.sql/read"]) || !actionsEqual(kv.actions, ["tinycloud.kv/get", "tinycloud.kv/list"])) return void 0;
322
+ if (hasCaveats(sql) || hasCaveats(kv)) return void 0;
323
+ const sqlSpace = ownerFromApplicationsSpace(sql.space);
324
+ const kvSpace = ownerFromApplicationsSpace(kv.space);
325
+ return sqlSpace !== void 0 && sqlSpace === kvSpace ? sqlSpace : void 0;
326
+ }
327
+ function ownerFromApplicationsSpace(space) {
328
+ const match = OWNER_APPLICATIONS_SPACE.exec(space);
329
+ if (!match) return void 0;
330
+ const chainId = Number(match[1]);
331
+ if (!Number.isSafeInteger(chainId)) return void 0;
332
+ return `did:pkh:eip155:${chainId}:${match[2].toLowerCase()}`;
333
+ }
334
+ function actionsEqual(actual, expected) {
335
+ const actualActions = [...actual].sort();
336
+ const expectedActions = [...expected].sort();
337
+ return actualActions.length === expectedActions.length && actualActions.every((value, index) => value === expectedActions[index]);
338
+ }
339
+ function hasCaveats(permission) {
340
+ return Object.hasOwn(permission, "caveats") && permission.caveats !== void 0;
341
+ }
342
+
343
+ // src/remote-store.ts
123
344
  var PROFILE = "agent";
124
345
  var MAX_CALLBACK_BYTES = 256 * 1024;
346
+ var NATIVE_GRANT_IMPORT_REJECTION_CODES = /* @__PURE__ */ new Set([
347
+ "DELEGATION_ARTIFACT_INVALID",
348
+ "DELEGATION_EXPIRED",
349
+ "DELEGATION_AUDIENCE_MISMATCH",
350
+ "DELEGATION_HOST_MISMATCH",
351
+ "DELEGATION_REJECTED"
352
+ ]);
125
353
  var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
126
354
  var BOOTSTRAP_PERMISSIONS = [
127
355
  {
@@ -163,6 +391,15 @@ var ApprovalRequestError = class extends Error {
163
391
  this.detail = detail;
164
392
  }
165
393
  };
394
+ var NativeGrantValidationError = class extends Error {
395
+ code;
396
+ constructor(status) {
397
+ const code = status.startsWith("native_") ? status : status === "required" ? "native_grant_required" : status === "expired" ? "native_grant_expired" : status === "unavailable" ? "native_grant_validation_unavailable" : "native_grant_rejected";
398
+ super(code);
399
+ this.name = "NativeGrantValidationError";
400
+ this.code = code;
401
+ }
402
+ };
166
403
  var RemoteTenantStore = class {
167
404
  #config;
168
405
  #initializing = /* @__PURE__ */ new Map();
@@ -175,9 +412,186 @@ var RemoteTenantStore = class {
175
412
  #scopedClearing = /* @__PURE__ */ new Map();
176
413
  #clearedScoped = /* @__PURE__ */ new Map();
177
414
  #approvalProcessing = /* @__PURE__ */ new Map();
415
+ #nativeIdentity;
416
+ #nativeGrantQueues = /* @__PURE__ */ new Map();
178
417
  constructor(config) {
179
418
  this.#config = config;
180
419
  }
420
+ async nativeServiceIdentity() {
421
+ const identity = await this.#loadNativeServiceKey();
422
+ return { serviceDid: normalizeDid(identity.did) };
423
+ }
424
+ nativeTenantStateRoot(ownerDid) {
425
+ const canonical = canonicalOwnerDid(ownerDid);
426
+ if (canonical === void 0) throw new NativeGrantValidationError("rejected");
427
+ const tenant = createHmac("sha256", this.#config.stateSecret).update(`native-tenant:${canonical}`).digest("hex");
428
+ return join(this.#config.stateDir, "native-tenants", tenant);
429
+ }
430
+ async acceptNativeGrant(serializedDelegation) {
431
+ if (Buffer.byteLength(serializedDelegation) > MAX_CALLBACK_BYTES) {
432
+ throw new NativeGrantValidationError("rejected");
433
+ }
434
+ const identity = await this.#loadNativeServiceKey();
435
+ const activated = await this.#activateNativeDelegation(serializedDelegation, identity);
436
+ const { ownerDid, spaceId, permissions, expiresAt } = activated;
437
+ return this.#withNativeGrantLock(ownerDid, async () => {
438
+ const stateRoot = this.nativeTenantStateRoot(ownerDid);
439
+ let previous = null;
440
+ try {
441
+ const stored = await readJson(this.#nativeGrantPath(stateRoot));
442
+ if (isRecord(stored) && stored.version === 1 && stored.ownerDid === ownerDid && typeof stored.serviceDid === "string" && samePrincipal(stored.serviceDid, identity.did) && typeof stored.serializedDelegation === "string" && typeof stored.delegationCid === "string" && typeof stored.spaceId === "string" && Array.isArray(stored.permissions) && stored.permissions.every(isPermissionEntry) && nativeOwnerForConnectorGrant(stored.permissions) === ownerDid && typeof stored.expiresAt === "string" && Number.isFinite(Date.parse(stored.expiresAt))) {
443
+ previous = stored;
444
+ }
445
+ } catch (error) {
446
+ if (!(error instanceof SyntaxError)) throw error;
447
+ }
448
+ if (previous !== null) {
449
+ const previousExpiry = Date.parse(previous.expiresAt);
450
+ const nextExpiry = expiresAt.getTime();
451
+ if (previousExpiry > nextExpiry || previousExpiry === nextExpiry && previous.delegationCid !== activated.cid) {
452
+ throw new NativeGrantValidationError("rejected");
453
+ }
454
+ if (previous.delegationCid === activated.cid) {
455
+ return {
456
+ ownerDid,
457
+ serviceDid: normalizeDid(identity.did),
458
+ expiresAt: previous.expiresAt,
459
+ stateRoot
460
+ };
461
+ }
462
+ }
463
+ const session = {
464
+ delegationHeader: activated.delegation.delegationHeader,
465
+ delegationCid: activated.cid,
466
+ spaceId,
467
+ jwk: identity.jwk,
468
+ verificationMethod: identity.did,
469
+ tinycloudHosts: [this.#config.nodeHost]
470
+ };
471
+ const grant = {
472
+ version: 1,
473
+ ownerDid,
474
+ serviceDid: normalizeDid(identity.did),
475
+ serializedDelegation: serializeDelegation(activated.delegation),
476
+ delegationCid: activated.cid,
477
+ spaceId,
478
+ permissions,
479
+ expiresAt: expiresAt.toISOString()
480
+ };
481
+ const sessionPath = this.#sessionPath(stateRoot);
482
+ const previousSession = await readOptionalText(sessionPath);
483
+ try {
484
+ await secureWrite(this.#profilePath(stateRoot), {
485
+ name: PROFILE,
486
+ host: this.#config.nodeHost,
487
+ chainId: activated.delegation.chainId,
488
+ spaceName: "applications",
489
+ did: identity.did,
490
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
491
+ profile: PROFILE,
492
+ operatorType: "agent",
493
+ posture: "delegate-session",
494
+ ownerDid,
495
+ sessionDid: identity.did,
496
+ spaceId,
497
+ tinycloudHost: this.#config.nodeHost
498
+ });
499
+ await secureWrite(this.#keyPath(stateRoot), identity.jwk);
500
+ await secureWrite(sessionPath, session);
501
+ const request = await withTinyCloudStateRoot(stateRoot, () => createOrReusePermissionRequest({
502
+ profile: PROFILE,
503
+ posture: "delegate-session",
504
+ operatorType: "agent",
505
+ host: this.#config.nodeHost,
506
+ sessionDid: identity.did,
507
+ ownerDid,
508
+ spaceId,
509
+ missing: permissions,
510
+ granted: [],
511
+ replace: true
512
+ }));
513
+ if (request.status !== "created") {
514
+ throw new NativeGrantValidationError("native_grant_validation_unavailable");
515
+ }
516
+ const imported = await invokeOperation(
517
+ "tinycloud.auth.import",
518
+ 1,
519
+ { profile: PROFILE, stateRoot },
520
+ {
521
+ kind: "tinycloud.auth.delegation",
522
+ version: 1,
523
+ requestId: request.request.requestId,
524
+ delegationCid: activated.cid,
525
+ delegation: activated.delegation
526
+ }
527
+ );
528
+ if (imported.status === "error") {
529
+ throw new NativeGrantValidationError(
530
+ NATIVE_GRANT_IMPORT_REJECTION_CODES.has(imported.error.code) ? "native_grant_rejected" : "native_grant_validation_unavailable"
531
+ );
532
+ }
533
+ if (imported.status !== "ok" || !isRecord(imported.output) || imported.output.activated !== true || imported.output.cid !== activated.cid) {
534
+ throw new NativeGrantValidationError("native_grant_validation_unavailable");
535
+ }
536
+ await secureWrite(this.#nativeGrantPath(stateRoot), grant);
537
+ } catch (error) {
538
+ try {
539
+ await restoreOptionalText(sessionPath, previousSession);
540
+ } catch {
541
+ await rm(this.#nativeGrantPath(stateRoot), { force: true }).catch(() => void 0);
542
+ throw new NativeGrantValidationError("native_grant_validation_unavailable");
543
+ }
544
+ throw error;
545
+ }
546
+ return { ownerDid, serviceDid: normalizeDid(identity.did), expiresAt: grant.expiresAt, stateRoot };
547
+ });
548
+ }
549
+ async nativeGrantForOwner(ownerDid) {
550
+ const canonical = canonicalOwnerDid(ownerDid);
551
+ if (canonical === void 0) return { status: "rejected" };
552
+ const stateRoot = this.nativeTenantStateRoot(canonical);
553
+ let stored;
554
+ try {
555
+ stored = await readJson(this.#nativeGrantPath(stateRoot));
556
+ } catch {
557
+ return { status: "rejected" };
558
+ }
559
+ if (stored === null) return { status: "required" };
560
+ if (!isRecord(stored) || stored.version !== 1 || stored.ownerDid !== canonical || typeof stored.serviceDid !== "string" || typeof stored.serializedDelegation !== "string" || typeof stored.delegationCid !== "string" || typeof stored.spaceId !== "string" || typeof stored.expiresAt !== "string" || !Array.isArray(stored.permissions) || !stored.permissions.every(isPermissionEntry) || nativeOwnerForConnectorGrant(stored.permissions) !== canonical) {
561
+ return { status: "rejected" };
562
+ }
563
+ const expires = Date.parse(stored.expiresAt);
564
+ if (!Number.isFinite(expires)) return { status: "rejected" };
565
+ if (expires <= Date.now()) return { status: "expired" };
566
+ try {
567
+ const identity = await this.#loadNativeServiceKey();
568
+ if (!samePrincipal(stored.serviceDid, identity.did)) return { status: "rejected" };
569
+ const activated = await this.#activateNativeDelegation(stored.serializedDelegation, identity);
570
+ if (activated.ownerDid !== canonical || activated.cid !== stored.delegationCid || activated.spaceId !== stored.spaceId || JSON.stringify(activated.permissions) !== JSON.stringify(stored.permissions) || activated.expiresAt.toISOString() !== stored.expiresAt) return { status: "rejected" };
571
+ return { status: "ok", ownerDid: canonical, stateRoot, expiresAt: stored.expiresAt };
572
+ } catch (error) {
573
+ if (error instanceof NativeGrantValidationError) {
574
+ return { status: error.code === "native_grant_expired" ? "expired" : error.code === "native_grant_validation_unavailable" ? "unavailable" : "rejected" };
575
+ }
576
+ return { status: "unavailable" };
577
+ }
578
+ }
579
+ async assertNativeGrantActive(ownerDid) {
580
+ const canonical = canonicalOwnerDid(ownerDid);
581
+ if (canonical === void 0) throw new NativeGrantValidationError("rejected");
582
+ const stateRoot = this.nativeTenantStateRoot(canonical);
583
+ let stored;
584
+ try {
585
+ stored = await readJson(this.#nativeGrantPath(stateRoot));
586
+ } catch {
587
+ throw new NativeGrantValidationError("rejected");
588
+ }
589
+ if (stored === null) throw new NativeGrantValidationError("required");
590
+ if (!isRecord(stored) || stored.version !== 1 || stored.ownerDid !== canonical || typeof stored.expiresAt !== "string" || !Number.isFinite(Date.parse(stored.expiresAt))) {
591
+ throw new NativeGrantValidationError("rejected");
592
+ }
593
+ if (Date.parse(stored.expiresAt) <= Date.now()) throw new NativeGrantValidationError("expired");
594
+ }
181
595
  tenantStateRoot(subject) {
182
596
  return join(this.#config.stateDir, "tenants", this.#tenantId(subject));
183
597
  }
@@ -729,7 +1143,7 @@ var RemoteTenantStore = class {
729
1143
  const expected = this.#signState(tenantId, nonce);
730
1144
  const supplied = Buffer.from(signature, "base64url");
731
1145
  const expectedBytes = Buffer.from(expected, "base64url");
732
- if (supplied.length !== expectedBytes.length || !timingSafeEqual(supplied, expectedBytes)) {
1146
+ if (supplied.length !== expectedBytes.length || !timingSafeEqual2(supplied, expectedBytes)) {
733
1147
  throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
734
1148
  }
735
1149
  const stateRoot = join(this.#config.stateDir, "tenants", tenantId);
@@ -780,6 +1194,123 @@ var RemoteTenantStore = class {
780
1194
  url.searchParams.set("state", `${pending.tenantId}.${pending.nonce}.${this.#signState(pending.tenantId, pending.nonce)}`);
781
1195
  return url.toString();
782
1196
  }
1197
+ async #withNativeGrantLock(ownerDid, action) {
1198
+ const previous = this.#nativeGrantQueues.get(ownerDid) ?? Promise.resolve();
1199
+ let releaseNext;
1200
+ const next = new Promise((resolve) => {
1201
+ releaseNext = resolve;
1202
+ });
1203
+ this.#nativeGrantQueues.set(ownerDid, next);
1204
+ await previous;
1205
+ try {
1206
+ return await action();
1207
+ } finally {
1208
+ releaseNext();
1209
+ if (this.#nativeGrantQueues.get(ownerDid) === next) this.#nativeGrantQueues.delete(ownerDid);
1210
+ }
1211
+ }
1212
+ async #loadNativeServiceKey() {
1213
+ if (this.#nativeIdentity === void 0) {
1214
+ const loading = this.#readOrCreateNativeServiceKey();
1215
+ this.#nativeIdentity = loading;
1216
+ void loading.catch(() => {
1217
+ if (this.#nativeIdentity === loading) this.#nativeIdentity = void 0;
1218
+ });
1219
+ }
1220
+ return this.#nativeIdentity;
1221
+ }
1222
+ async #readOrCreateNativeServiceKey() {
1223
+ const path = join(this.#config.stateDir, ".tinycloud", "native-service-key.json");
1224
+ await secureDirectory(dirname(path));
1225
+ let stored = await readJson(path);
1226
+ if (stored === null) {
1227
+ const generated = generateSessionKey();
1228
+ try {
1229
+ await writeFile(path, `${JSON.stringify({ version: 1, jwk: generated.jwk })}
1230
+ `, {
1231
+ mode: 384,
1232
+ flag: "wx"
1233
+ });
1234
+ } catch (error) {
1235
+ if (!isRecord(error) || error.code !== "EEXIST") throw error;
1236
+ }
1237
+ await chmod(path, 384);
1238
+ stored = await readJson(path);
1239
+ }
1240
+ if (stored === null || !isRecord(stored) || stored.version !== 1 || !isRecord(stored.jwk)) {
1241
+ throw new Error("The native MCP service identity is unavailable.");
1242
+ }
1243
+ return { did: didFromJwk(stored.jwk), jwk: stored.jwk };
1244
+ }
1245
+ async #activateNativeDelegation(serialized, identity) {
1246
+ let delegation;
1247
+ try {
1248
+ const parsed = JSON.parse(serialized);
1249
+ if (!isRecord(parsed)) throw new Error("invalid delegation");
1250
+ delegation = deserializeDelegation(serialized);
1251
+ } catch {
1252
+ throw new NativeGrantValidationError("native_grant_rejected");
1253
+ }
1254
+ if (!isRecord(delegation.delegationHeader) || typeof delegation.delegationHeader.Authorization !== "string" || delegation.delegationHeader.Authorization.length === 0 || typeof delegation.cid !== "string" || delegation.cid.length === 0 || typeof delegation.delegateDID !== "string" || !samePrincipal(delegation.delegateDID, normalizeDid(identity.did)) || typeof delegation.spaceId !== "string" || typeof delegation.ownerAddress !== "string" || !Number.isSafeInteger(delegation.chainId) || typeof delegation.host !== "string" || !sameNodeHost(delegation.host, this.#config.nodeHost) || !(delegation.expiry instanceof Date) || !Number.isFinite(delegation.expiry.getTime())) {
1255
+ throw new NativeGrantValidationError("native_grant_rejected");
1256
+ }
1257
+ const now = Date.now();
1258
+ const claimedExpiry = delegation.expiry.getTime();
1259
+ if (claimedExpiry <= now) throw new NativeGrantValidationError("native_grant_expired");
1260
+ if (claimedExpiry > now + NATIVE_GRANT_MAX_LIFETIME_SECONDS * 1e3) {
1261
+ throw new NativeGrantValidationError("native_grant_rejected");
1262
+ }
1263
+ let ownerDid;
1264
+ let spaceId;
1265
+ const node = new TinyCloudNode({ host: this.#config.nodeHost });
1266
+ try {
1267
+ await node.restoreSession({
1268
+ delegationHeader: delegation.delegationHeader,
1269
+ delegationCid: delegation.cid,
1270
+ spaceId: delegation.spaceId,
1271
+ jwk: identity.jwk,
1272
+ verificationMethod: identity.did,
1273
+ tinycloudHosts: [this.#config.nodeHost]
1274
+ });
1275
+ const activated = await activateValidatedRuntimeDelegation(node, delegation, {
1276
+ host: this.#config.nodeHost,
1277
+ authorize: (permissions) => {
1278
+ const derivedOwner = nativeOwnerForConnectorGrant(permissions);
1279
+ const signedSpace = permissions[0]?.space;
1280
+ if (derivedOwner === void 0 || signedSpace === void 0) return false;
1281
+ const metadataOwner = canonicalOwnerDid(
1282
+ `did:pkh:eip155:${delegation.chainId}:${delegation.ownerAddress}`
1283
+ );
1284
+ if (metadataOwner !== derivedOwner || delegation.spaceId.toLowerCase() !== signedSpace.toLowerCase()) return false;
1285
+ ownerDid = derivedOwner;
1286
+ spaceId = signedSpace;
1287
+ return true;
1288
+ }
1289
+ });
1290
+ if (ownerDid === void 0 || spaceId === void 0 || !samePrincipal(activated.audience, normalizeDid(identity.did)) || activated.expiry.getTime() > now + NATIVE_GRANT_MAX_LIFETIME_SECONDS * 1e3) {
1291
+ throw new NativeGrantValidationError("native_grant_rejected");
1292
+ }
1293
+ return {
1294
+ ownerDid,
1295
+ spaceId,
1296
+ permissions: [...activated.effectivePermissions],
1297
+ cid: activated.cid,
1298
+ expiresAt: activated.expiry,
1299
+ delegation: activated.delegation
1300
+ };
1301
+ } catch (error) {
1302
+ if (error instanceof NativeGrantValidationError) throw error;
1303
+ if (error instanceof RuntimeDelegationRejectedError) {
1304
+ throw new NativeGrantValidationError(
1305
+ error.reason === "expired" ? "native_grant_expired" : error.reason === "activation_unavailable" ? "native_grant_validation_unavailable" : "native_grant_rejected"
1306
+ );
1307
+ }
1308
+ throw new NativeGrantValidationError("native_grant_validation_unavailable");
1309
+ }
1310
+ }
1311
+ #nativeGrantPath(stateRoot) {
1312
+ return join(stateRoot, ".tinycloud", "native-grant.json");
1313
+ }
783
1314
  #tenantId(subject) {
784
1315
  return createHmac("sha256", this.#config.stateSecret).update(`tenant:${subject}`).digest("hex");
785
1316
  }
@@ -873,6 +1404,21 @@ function samePrincipal(left, right) {
873
1404
  function normalizeDid(value) {
874
1405
  return value.split("#", 1)[0];
875
1406
  }
1407
+ function sameNodeHost(left, right) {
1408
+ try {
1409
+ const normalize = (value) => {
1410
+ const url = new URL(value);
1411
+ if (url.username !== "" || url.password !== "") return "";
1412
+ url.hash = "";
1413
+ url.search = "";
1414
+ url.pathname = url.pathname.replace(/\/+$/, "");
1415
+ return url.toString().replace(/\/+$/, "");
1416
+ };
1417
+ return normalize(left) !== "" && normalize(left) === normalize(right);
1418
+ } catch {
1419
+ return false;
1420
+ }
1421
+ }
876
1422
  function isPermissionEntry(value) {
877
1423
  return isRecord(value) && typeof value.service === "string" && typeof value.space === "string" && typeof value.path === "string" && Array.isArray(value.actions) && value.actions.every((action) => typeof action === "string");
878
1424
  }
@@ -903,6 +1449,23 @@ async function readJson(path) {
903
1449
  throw error;
904
1450
  }
905
1451
  }
1452
+ async function readOptionalText(path) {
1453
+ try {
1454
+ return await readFile(path, "utf8");
1455
+ } catch (error) {
1456
+ if (isRecord(error) && error.code === "ENOENT") return null;
1457
+ throw error;
1458
+ }
1459
+ }
1460
+ async function restoreOptionalText(path, contents) {
1461
+ if (contents === null) {
1462
+ await rm(path, { force: true });
1463
+ return;
1464
+ }
1465
+ await secureDirectory(dirname(path));
1466
+ await writeFile(path, contents, { mode: 384 });
1467
+ await chmod(path, 384);
1468
+ }
906
1469
  async function reusablePending(directory, expected) {
907
1470
  let names;
908
1471
  try {
@@ -1299,7 +1862,7 @@ function createJsonSchemaValidator() {
1299
1862
  return new AjvJsonSchemaValidator();
1300
1863
  }
1301
1864
  function registerTinyCloudTools(server, selection, validator) {
1302
- const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface));
1865
+ const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface) && (selection.allowedTools === void 0 || selection.allowedTools.includes(binding.name)));
1303
1866
  for (const binding of bindings) {
1304
1867
  const operation = definitions.get(`${binding.operationId}@${binding.operationVersion}`);
1305
1868
  if (operation === void 0) {
@@ -1377,7 +1940,7 @@ function outcomeCode(result) {
1377
1940
  // package.json
1378
1941
  var package_default = {
1379
1942
  name: "@tinycloud/mcp",
1380
- version: "0.8.3-beta.2",
1943
+ version: "0.8.3-beta.21",
1381
1944
  description: "TinyCloud delegated operations MCP server",
1382
1945
  keywords: [
1383
1946
  "tinycloud",
@@ -1448,9 +2011,9 @@ var package_default = {
1448
2011
  dependencies: {
1449
2012
  "@modelcontextprotocol/node": "2.0.0-beta.5",
1450
2013
  "@modelcontextprotocol/server": "2.0.0-beta.5",
1451
- "@tinycloud/node-sdk": "3.8.0-beta.2",
2014
+ "@tinycloud/node-sdk": "3.8.0-beta.19",
1452
2015
  "@tinycloud/node-sdk-wasm": "1.7.8",
1453
- "@tinycloud/operations": "0.7.7-beta.2",
2016
+ "@tinycloud/operations": "0.7.7-beta.18",
1454
2017
  jose: "6.2.4"
1455
2018
  },
1456
2019
  devDependencies: {
@@ -1474,15 +2037,22 @@ var MCP_SERVER_NAME = "tinycloud-mcp";
1474
2037
 
1475
2038
  // src/http.ts
1476
2039
  var REQUIRED_SCOPE = "tinycloud:mcp";
2040
+ var LOCAL_CLI_PACKAGE = ["@tinycloud", "cli"].join("/");
2041
+ var LOCAL_MCP_PACKAGE = ["@tinycloud", "mcp"].join("/");
1477
2042
  function createHostedMcpApp(config) {
2043
+ if (config.nativeServiceCredential !== void 0 && Buffer.byteLength(config.nativeServiceCredential, "utf8") < 32) {
2044
+ throw new Error("TC_MCP_NATIVE_SERVICE_CREDENTIAL must contain at least 32 bytes.");
2045
+ }
1478
2046
  const resourceUrl = canonicalResourceUrl(config.publicUrl);
1479
2047
  const pluginUrl = new URL("./chatgpt/mcp", resourceUrl);
1480
- const hosted = createHostedSurface(config, resourceUrl, "hosted");
2048
+ const statusFor = (resource) => config.introspection === void 0 ? void 0 : createTokenStatusChecker({ ...config.introspection, resourceUrl: resource.toString() });
2049
+ const hosted = createHostedSurface(config, resourceUrl, "hosted", statusFor(resourceUrl));
1481
2050
  const plugin = createHostedSurface({
1482
2051
  ...config,
2052
+ nativeServiceCredential: void 0,
1483
2053
  stateDir: join3(config.stateDir, "chatgpt"),
1484
2054
  approvalPath: new URL("./connect", pluginUrl).pathname
1485
- }, pluginUrl, "public");
2055
+ }, pluginUrl, "public", statusFor(pluginUrl));
1486
2056
  const pluginPaths = {
1487
2057
  [pluginUrl.pathname]: true,
1488
2058
  [new URL("./connect", pluginUrl).pathname]: true,
@@ -1496,7 +2066,7 @@ function createHostedMcpApp(config) {
1496
2066
  }
1497
2067
  };
1498
2068
  }
1499
- function createHostedSurface(config, resourceUrl, surface) {
2069
+ function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
1500
2070
  const connectPath = config.approvalPath ?? "/connect";
1501
2071
  const callbackPath = `${connectPath}/callback`;
1502
2072
  const issuer = String(config.oauthMetadata.issuer);
@@ -1505,7 +2075,8 @@ function createHostedSurface(config, resourceUrl, surface) {
1505
2075
  issuer,
1506
2076
  audience: resourceUrl.toString(),
1507
2077
  jwksUri,
1508
- requiredScope: REQUIRED_SCOPE
2078
+ requiredScope: REQUIRED_SCOPE,
2079
+ ...tokenStatus === void 0 ? {} : { tokenStatus }
1509
2080
  });
1510
2081
  const resourceMetadataUrl = getOAuthProtectedResourceMetadataUrl(resourceUrl);
1511
2082
  const authenticate = requireBearerAuth({
@@ -1514,16 +2085,19 @@ function createHostedSurface(config, resourceUrl, surface) {
1514
2085
  resourceMetadataUrl
1515
2086
  });
1516
2087
  const store = new RemoteTenantStore({ ...config, publicUrl: resourceUrl });
2088
+ const nativeServiceIdentityEnabled = surface === "hosted" && config.nativeServiceCredential !== void 0;
2089
+ if (nativeServiceIdentityEnabled) void store.nativeServiceIdentity().catch(() => void 0);
1517
2090
  const handler = createMcpHandler(async ({ authInfo: authInfo2 }) => {
1518
- const identity = oauthIdentity(authInfo2);
2091
+ const nativeOwner = typeof authInfo2?.extra?.nativeOwnerDid === "string" ? authInfo2.extra.nativeOwnerDid : void 0;
2092
+ const identity = nativeOwner === void 0 ? oauthIdentity(authInfo2) : { subject: `native:${nativeOwner}`, ownerDids: [nativeOwner] };
1519
2093
  const { subject, ownerDids } = identity;
1520
- const stateRoot = await store.initializeTenant(subject);
2094
+ const stateRoot = nativeOwner === void 0 ? await store.initializeTenant(subject) : store.nativeTenantStateRoot(nativeOwner);
1521
2095
  const validator = createJsonSchemaValidator();
1522
2096
  const server = new McpServer2(
1523
2097
  { name: MCP_SERVER_NAME, version: MCP_VERSION },
1524
2098
  {
1525
2099
  jsonSchemaValidator: validator,
1526
- instructions: surface === "public" ? "Use TinyCloud only for the user's requested task. Call tinycloud_connect first. If an operation returns authority_required, present approval.url and wait for the user's approval before retrying. If approval is declined, stop that operation. For personal records, call tinycloud_apps_context first. Never request or store passwords, API keys, authentication codes, government identifiers, payment-card data, or protected health information." : "Call tinycloud_connect first. If an operation returns authority_required, open approval.url, then retry that operation unchanged. For personal records such as weight, workouts, goals or todos, call tinycloud_apps_context first."
2100
+ instructions: nativeOwner === void 0 ? surface === "public" ? "Use TinyCloud only for the user's requested task. Call tinycloud_connect first. If an operation returns authority_required, present approval.url and wait for the user's approval before retrying. If approval is declined, stop that operation. For personal records, call tinycloud_apps_context first. Never request or store passwords, API keys, authentication codes, government identifiers, payment-card data, or protected health information." : "Call tinycloud_connect first. If an operation returns authority_required, open approval.url, then retry that operation unchanged. For personal records such as weight, workouts, goals or todos, call tinycloud_apps_context first." : "Use only the user's requested connector-transcript data. The signed TinyCloud grant authorizes read-only SQL and transcript KV reads."
1527
2101
  }
1528
2102
  );
1529
2103
  registerTinyCloudTools(server, {
@@ -1532,10 +2106,15 @@ function createHostedSurface(config, resourceUrl, surface) {
1532
2106
  explicitProfile: true,
1533
2107
  allowOwnerProfile: false,
1534
2108
  stateRoot,
1535
- beforeOperation: () => store.validateTenant(subject, ownerDids),
1536
- transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
2109
+ ...nativeOwner === void 0 ? {} : {
2110
+ allowedTools: ["tinycloud_sql_query", "tinycloud_kv_get"]
2111
+ },
2112
+ beforeOperation: nativeOwner === void 0 ? () => store.validateTenant(subject, ownerDids) : () => store.assertNativeGrantActive(nativeOwner),
2113
+ ...nativeOwner === void 0 ? {
2114
+ transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
2115
+ } : {}
1537
2116
  }, validator);
1538
- registerConnectTool(server, validator, store, identity, surface);
2117
+ if (nativeOwner === void 0) registerConnectTool(server, validator, store, identity, surface);
1539
2118
  return server;
1540
2119
  }, {
1541
2120
  legacy: "stateless",
@@ -1551,14 +2130,205 @@ function createHostedSurface(config, resourceUrl, surface) {
1551
2130
  const allowedHosts = [resourceUrl.hostname];
1552
2131
  return {
1553
2132
  async fetch(request) {
1554
- const metadata = oauthMetadataResponse(request, metadataOptions);
1555
- if (metadata !== void 0) return metadata;
1556
2133
  const url = new URL(request.url);
2134
+ if (url.pathname !== "/healthz") {
2135
+ const hostFailure = hostHeaderValidationResponse(request, allowedHosts);
2136
+ if (hostFailure !== void 0) return hostFailure;
2137
+ }
2138
+ if (url.pathname === "/.well-known/oauth-protected-resource" && surface === "hosted") {
2139
+ const metadataUrl = new URL(getOAuthProtectedResourceMetadataUrl(resourceUrl));
2140
+ const metadataRequest = new Request(metadataUrl.toString(), {
2141
+ method: request.method,
2142
+ headers: request.headers
2143
+ });
2144
+ const response = oauthMetadataResponse(metadataRequest, metadataOptions);
2145
+ if (response !== void 0) return withBearerMethodsSupported(response);
2146
+ }
2147
+ const metadata = oauthMetadataResponse(request, metadataOptions);
2148
+ if (metadata !== void 0) {
2149
+ return surface === "hosted" ? withBearerMethodsSupported(metadata) : metadata;
2150
+ }
2151
+ if (url.pathname === "/.well-known/ai-catalog.json" && surface === "hosted") {
2152
+ if (request.method === "OPTIONS") {
2153
+ return publicMetadataCors(new Response(null, { status: 204 }), request);
2154
+ }
2155
+ if (request.method !== "GET") return methodNotAllowed("GET");
2156
+ return publicMetadataCors(Response.json({
2157
+ specVersion: "1.0",
2158
+ host: {
2159
+ displayName: "TinyCloud, Inc.",
2160
+ identifier: "https://mcp.tinycloud.xyz",
2161
+ documentationUrl: "https://docs.tinycloud.xyz/cli/mcp"
2162
+ },
2163
+ entries: [{
2164
+ identifier: "urn:air:tinycloud.xyz:mcp:tinycloud-mcp",
2165
+ displayName: "TinyCloud MCP",
2166
+ type: "application/json",
2167
+ description: "Hosted Streamable HTTP MCP server for delegated TinyCloud operations.",
2168
+ url: new URL("/.well-known/mcp/server-card.json", resourceUrl).toString(),
2169
+ representativeQueries: [
2170
+ "How do I connect to TinyCloud's hosted MCP server?",
2171
+ "Which endpoint provides TinyCloud's delegated MCP operations?",
2172
+ "How can I connect to TinyCloud MCP using local stdio?"
2173
+ ]
2174
+ }]
2175
+ }, {
2176
+ headers: {
2177
+ "cache-control": "public, max-age=300",
2178
+ "content-type": "application/json; charset=utf-8"
2179
+ }
2180
+ }), request);
2181
+ }
2182
+ if (url.pathname === "/.well-known/poppy.json" && surface === "hosted") {
2183
+ if (request.method === "OPTIONS") {
2184
+ return publicMetadataCors(new Response(null, { status: 204 }), request);
2185
+ }
2186
+ if (request.method !== "GET") return methodNotAllowed("GET");
2187
+ return publicMetadataCors(Response.json({
2188
+ protocol_version: "0.1",
2189
+ organization: {
2190
+ name: "TinyCloud, Inc.",
2191
+ domain: resourceUrl.hostname
2192
+ },
2193
+ web: {}
2194
+ }, {
2195
+ headers: {
2196
+ "cache-control": "public, max-age=300",
2197
+ "content-type": "application/json; charset=utf-8"
2198
+ }
2199
+ }), request);
2200
+ }
2201
+ if (url.pathname === "/auth.md" && surface === "hosted") {
2202
+ if (request.method === "OPTIONS") {
2203
+ return publicMetadataCors(new Response(null, { status: 204 }), request);
2204
+ }
2205
+ if (request.method !== "GET") return methodNotAllowed("GET");
2206
+ const issuerUrl = new URL(issuer);
2207
+ const authorizationServerMetadataUrl = new URL(
2208
+ `/.well-known/oauth-authorization-server${issuerUrl.pathname}`,
2209
+ issuerUrl.origin
2210
+ );
2211
+ const registrationEndpoint = config.oauthMetadata.registration_endpoint;
2212
+ const registrationNote = typeof registrationEndpoint === "string" ? `OpenKey's OAuth \`registration_endpoint\` is ${registrationEndpoint}; it registers OAuth clients, not TinyCloud users or agents.` : "The authorization server does not advertise an OAuth client registration endpoint.";
2213
+ const authDoc = [
2214
+ "# TinyCloud MCP auth.md",
2215
+ "",
2216
+ `Protected resource: ${resourceUrl}.`,
2217
+ "",
2218
+ "Send an OAuth access token in the `Authorization: Bearer` header.",
2219
+ "",
2220
+ `Protected Resource Metadata: ${new URL("/.well-known/oauth-protected-resource", resourceUrl)}.`,
2221
+ `Authorization server issuer: ${issuer}.`,
2222
+ `Authorization server metadata: ${authorizationServerMetadataUrl}.`,
2223
+ `Scope: \`${REQUIRED_SCOPE}\`.`,
2224
+ "",
2225
+ "Hosted access uses OpenKey OAuth.",
2226
+ registrationNote,
2227
+ "TinyCloud does not host an Auth.md agent-enrollment or user-registration endpoint for this service.",
2228
+ "No `agent_auth` registration block is published because no agent-registration endpoint is available.",
2229
+ "",
2230
+ `Local stdio is a separate option configured with \`${LOCAL_CLI_PACKAGE}\` and \`${LOCAL_MCP_PACKAGE}\`.`,
2231
+ "",
2232
+ "Connection steps: https://docs.tinycloud.xyz/cli/mcp"
2233
+ ].join("\n") + "\n";
2234
+ return publicMetadataCors(publicText(authDoc, "text/markdown; charset=utf-8"), request);
2235
+ }
2236
+ if (url.pathname === "/robots.txt" && surface === "hosted") {
2237
+ return request.method === "GET" ? publicText(`User-agent: *
2238
+ Allow: /
2239
+ Sitemap: ${new URL("/sitemap.xml", resourceUrl)}
2240
+ Agentmap: ${new URL("/.well-known/ai-catalog.json", resourceUrl)}
2241
+ `, "text/plain; charset=utf-8") : methodNotAllowed("GET");
2242
+ }
2243
+ if ((url.pathname === "/mcp/server-card" || url.pathname === "/.well-known/mcp/server-card.json") && surface === "hosted") {
2244
+ if (request.method === "OPTIONS") {
2245
+ return publicMetadataCors(new Response(null, { status: 204 }), request);
2246
+ }
2247
+ if (request.method !== "GET") return methodNotAllowed("GET");
2248
+ const identity = nativeServiceIdentityEnabled ? await store.nativeServiceIdentity().catch(() => void 0) : void 0;
2249
+ const card = {
2250
+ $schema: "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json",
2251
+ name: `xyz.tinycloud/${MCP_SERVER_NAME}`,
2252
+ title: "TinyCloud MCP",
2253
+ version: MCP_VERSION,
2254
+ description: "MCP server for delegated TinyCloud operations.",
2255
+ websiteUrl: "https://docs.tinycloud.xyz/cli/mcp",
2256
+ repository: { source: "github", url: "https://github.com/TinyCloudLabs/js-sdk", subfolder: "packages/mcp" },
2257
+ remotes: [{ type: "streamable-http", url: resourceUrl.toString() }]
2258
+ };
2259
+ if (identity !== void 0) {
2260
+ card["x-tinycloud-native-auth"] = {
2261
+ serviceDid: identity.serviceDid,
2262
+ serviceDidField: "x-tinycloud-native-auth.serviceDid",
2263
+ grantEndpoint: {
2264
+ method: "POST",
2265
+ path: "/native/grant",
2266
+ body: { delegation: "serialized PortableDelegation JSON string" }
2267
+ },
2268
+ serviceCredentialHeader: NATIVE_SERVICE_CREDENTIAL_HEADER,
2269
+ serviceCredentialEnv: "TC_MCP_NATIVE_SERVICE_CREDENTIAL",
2270
+ ownerDidHeader: NATIVE_OWNER_DID_HEADER,
2271
+ maxGrantLifetimeSeconds: NATIVE_GRANT_MAX_LIFETIME_SECONDS,
2272
+ acceptedCapabilities: [
2273
+ { service: "tinycloud.sql", space: "applications", path: "xyz.tinycloud.tinychat/connectors", actions: ["tinycloud.sql/read"] },
2274
+ { service: "tinycloud.kv", space: "applications", path: "xyz.tinycloud.tinychat/connectors/", actions: ["tinycloud.kv/get", "tinycloud.kv/list"] }
2275
+ ],
2276
+ tools: ["tinycloud_sql_query", "tinycloud_kv_get"],
2277
+ errors: {
2278
+ required: NATIVE_GRANT_REQUIRED,
2279
+ expired: NATIVE_GRANT_EXPIRED,
2280
+ rejected: NATIVE_GRANT_REJECTED,
2281
+ serviceUnauthorized: NATIVE_SERVICE_UNAUTHORIZED,
2282
+ serviceUnavailable: NATIVE_SERVICE_UNAVAILABLE,
2283
+ validationUnavailable: NATIVE_GRANT_VALIDATION_UNAVAILABLE
2284
+ }
2285
+ };
2286
+ }
2287
+ return publicMetadataCors(Response.json(card, {
2288
+ headers: { "cache-control": "public, max-age=300" }
2289
+ }), request);
2290
+ }
2291
+ if (url.pathname === "/llms.txt" && surface === "hosted") {
2292
+ return request.method === "GET" ? publicText(`# TinyCloud MCP
2293
+
2294
+ Hosted Streamable HTTP endpoint: https://mcp.tinycloud.xyz/mcp (OpenKey OAuth).
2295
+
2296
+ Connection documentation: https://docs.tinycloud.xyz/cli/mcp
2297
+
2298
+ Local alternative: install ${LOCAL_CLI_PACKAGE} and ${LOCAL_MCP_PACKAGE}, then set up tinycloud-mcp as a stdio server in your MCP client.
2299
+ `, "text/plain; charset=utf-8") : methodNotAllowed("GET");
2300
+ }
2301
+ if (url.pathname === "/sitemap.xml" && surface === "hosted") {
2302
+ const sitemap = `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><url><loc>${new URL("/", resourceUrl)}</loc></url></urlset>`;
2303
+ return request.method === "GET" ? publicText(sitemap, "application/xml; charset=utf-8") : methodNotAllowed("GET");
2304
+ }
2305
+ if (url.pathname === "/" && surface === "hosted") {
2306
+ if (request.method !== "GET" && request.method !== "HEAD") return methodNotAllowed("GET, HEAD");
2307
+ const markdown = request.headers.get("accept")?.includes("text/markdown") ?? false;
2308
+ const content = markdown ? `# TinyCloud MCP
2309
+
2310
+ Hosted endpoint: <https://mcp.tinycloud.xyz/mcp> (Streamable HTTP, OpenKey OAuth).
2311
+
2312
+ [Connection documentation](https://docs.tinycloud.xyz/cli/mcp).
2313
+
2314
+ Local alternative: install \`${LOCAL_CLI_PACKAGE}\` and \`${LOCAL_MCP_PACKAGE}\` and connect the \`tinycloud-mcp\` stdio server.
2315
+ ` : `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="ai-catalog" href="/.well-known/ai-catalog.json"><title>TinyCloud MCP</title></head><main><h1>TinyCloud MCP</h1><p>Hosted MCP endpoint: <code>https://mcp.tinycloud.xyz/mcp</code> (Streamable HTTP, OpenKey OAuth).</p><p><a href="https://docs.tinycloud.xyz/cli/mcp">Connection documentation</a></p><p>Local alternative: install <code>${LOCAL_CLI_PACKAGE}</code> and <code>${LOCAL_MCP_PACKAGE}</code> and connect the <code>tinycloud-mcp</code> stdio server.</p></main></html>`;
2316
+ const headers = new Headers({
2317
+ "content-type": markdown ? "text/markdown; charset=utf-8" : "text/html; charset=utf-8",
2318
+ "cache-control": "public, max-age=300",
2319
+ "content-security-policy": "default-src 'none'; base-uri 'none'; frame-ancestors 'none'",
2320
+ "x-content-type-options": "nosniff",
2321
+ vary: "Accept"
2322
+ });
2323
+ headers.set(
2324
+ "link",
2325
+ `<${new URL("/mcp/server-card", resourceUrl)}>; rel="describedby"; type="application/json", <https://docs.tinycloud.xyz/cli/mcp>; rel="service-doc"`
2326
+ );
2327
+ return new Response(request.method === "HEAD" ? null : content, { headers });
2328
+ }
1557
2329
  if (url.pathname === "/healthz") {
1558
2330
  return request.method === "GET" ? json({ ok: true }) : methodNotAllowed("GET");
1559
2331
  }
1560
- const hostFailure = hostHeaderValidationResponse(request, allowedHosts);
1561
- if (hostFailure !== void 0) return hostFailure;
1562
2332
  if (url.pathname === connectPath) {
1563
2333
  if (request.method !== "GET") return methodNotAllowed("GET");
1564
2334
  try {
@@ -1614,6 +2384,73 @@ function createHostedSurface(config, resourceUrl, surface) {
1614
2384
  );
1615
2385
  }
1616
2386
  }
2387
+ if (url.pathname === "/native/grant" && surface === "hosted") {
2388
+ if (request.method !== "POST") return methodNotAllowed("POST");
2389
+ if (request.headers.has("origin")) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
2390
+ if (config.nativeServiceCredential === void 0) {
2391
+ return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
2392
+ }
2393
+ if (!serviceCredentialMatches(
2394
+ request.headers.get(NATIVE_SERVICE_CREDENTIAL_HEADER),
2395
+ config.nativeServiceCredential
2396
+ )) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
2397
+ if (!request.headers.get("content-type")?.toLowerCase().startsWith("application/json")) {
2398
+ return json({ error: NATIVE_GRANT_REJECTED }, 401);
2399
+ }
2400
+ const bodyText = await readBoundedBodyText(request, MAX_NATIVE_GRANT_BODY_BYTES);
2401
+ if (bodyText === void 0) return json({ error: NATIVE_GRANT_REJECTED }, 401);
2402
+ let body;
2403
+ try {
2404
+ body = JSON.parse(bodyText);
2405
+ } catch {
2406
+ return json({ error: NATIVE_GRANT_REJECTED }, 401);
2407
+ }
2408
+ if (typeof body !== "object" || body === null || Array.isArray(body) || !("delegation" in body)) {
2409
+ return json({ error: NATIVE_GRANT_REJECTED }, 401);
2410
+ }
2411
+ const grantBody = body;
2412
+ if (Object.keys(body).length !== 1 || typeof grantBody.delegation !== "string") {
2413
+ return json({ error: NATIVE_GRANT_REJECTED }, 401);
2414
+ }
2415
+ try {
2416
+ const accepted = await store.acceptNativeGrant(grantBody.delegation);
2417
+ return json({
2418
+ ownerDid: accepted.ownerDid,
2419
+ serviceDid: accepted.serviceDid,
2420
+ expiresAt: accepted.expiresAt
2421
+ }, 201);
2422
+ } catch (error) {
2423
+ return nativeGrantErrorResponse(error);
2424
+ }
2425
+ }
2426
+ if (url.pathname === resourceUrl.pathname && surface === "hosted" && (request.headers.has(NATIVE_SERVICE_CREDENTIAL_HEADER) || request.headers.has(NATIVE_OWNER_DID_HEADER))) {
2427
+ if (config.nativeServiceCredential === void 0) {
2428
+ return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
2429
+ }
2430
+ if (!serviceCredentialMatches(
2431
+ request.headers.get(NATIVE_SERVICE_CREDENTIAL_HEADER),
2432
+ config.nativeServiceCredential
2433
+ )) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
2434
+ if (request.headers.has("origin")) return json({ error: NATIVE_SERVICE_UNAUTHORIZED }, 403);
2435
+ const ownerHeader = request.headers.get(NATIVE_OWNER_DID_HEADER);
2436
+ if (ownerHeader === null || ownerHeader.length === 0) {
2437
+ return json({ error: NATIVE_GRANT_REQUIRED }, 401);
2438
+ }
2439
+ const grant = await store.nativeGrantForOwner(ownerHeader);
2440
+ if (grant.status !== "ok") {
2441
+ const status = grant.status === "unavailable" ? 503 : 401;
2442
+ const code = grant.status === "required" ? NATIVE_GRANT_REQUIRED : grant.status === "expired" ? NATIVE_GRANT_EXPIRED : grant.status === "unavailable" ? NATIVE_GRANT_VALIDATION_UNAVAILABLE : NATIVE_GRANT_REJECTED;
2443
+ return json({ error: code }, status);
2444
+ }
2445
+ return handler.fetch(request, {
2446
+ authInfo: {
2447
+ token: "native-service",
2448
+ clientId: "tinycloud-native",
2449
+ scopes: [],
2450
+ extra: { nativeOwnerDid: grant.ownerDid }
2451
+ }
2452
+ });
2453
+ }
1617
2454
  if (url.pathname !== resourceUrl.pathname) return json({ error: "Not found" }, 404);
1618
2455
  const originFailure = validateOrigin(request, config.allowedOrigins);
1619
2456
  if (originFailure !== void 0) return originFailure;
@@ -1725,9 +2562,77 @@ function callbackCors(response, openkeyHost) {
1725
2562
  headers.set("vary", "Origin");
1726
2563
  return new Response(response.body, { status: response.status, headers });
1727
2564
  }
2565
+ async function readBoundedBodyText(request, maxBytes) {
2566
+ const contentLength = request.headers.get("content-length");
2567
+ if (contentLength !== null) {
2568
+ const parsedLength = Number(contentLength);
2569
+ if (Number.isFinite(parsedLength) && parsedLength > maxBytes) return void 0;
2570
+ }
2571
+ if (request.body === null) return "";
2572
+ const reader = request.body.getReader();
2573
+ const chunks = [];
2574
+ let total = 0;
2575
+ try {
2576
+ while (true) {
2577
+ const { done, value } = await reader.read();
2578
+ if (done) break;
2579
+ total += value.byteLength;
2580
+ if (total > maxBytes) {
2581
+ await reader.cancel();
2582
+ return void 0;
2583
+ }
2584
+ chunks.push(value);
2585
+ }
2586
+ } finally {
2587
+ reader.releaseLock();
2588
+ }
2589
+ return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
2590
+ }
2591
+ function nativeGrantErrorResponse(error) {
2592
+ if (!(error instanceof NativeGrantValidationError)) {
2593
+ return json({ error: NATIVE_SERVICE_UNAVAILABLE }, 503);
2594
+ }
2595
+ const unavailable = error.code === NATIVE_GRANT_VALIDATION_UNAVAILABLE || error.code === NATIVE_SERVICE_UNAVAILABLE;
2596
+ return json({ error: error.code }, unavailable ? 503 : 401);
2597
+ }
1728
2598
  function json(value, status = 200) {
1729
2599
  return Response.json(value, { status, headers: { "cache-control": "no-store" } });
1730
2600
  }
2601
+ async function withBearerMethodsSupported(response) {
2602
+ if (response.status === 204 || response.body === null) return response;
2603
+ const metadata = await response.json();
2604
+ const headers = new Headers(response.headers);
2605
+ headers.delete("content-length");
2606
+ headers.delete("content-encoding");
2607
+ return new Response(JSON.stringify({ ...metadata, bearer_methods_supported: ["header"] }), {
2608
+ status: response.status,
2609
+ statusText: response.statusText,
2610
+ headers
2611
+ });
2612
+ }
2613
+ function publicMetadataCors(response, request) {
2614
+ const headers = new Headers(response.headers);
2615
+ headers.set("access-control-allow-origin", "*");
2616
+ headers.set("access-control-allow-methods", "GET, OPTIONS");
2617
+ const requestedHeaders = request.method === "OPTIONS" ? request.headers.get("access-control-request-headers") : null;
2618
+ if (requestedHeaders !== null) {
2619
+ headers.set("access-control-allow-headers", requestedHeaders);
2620
+ }
2621
+ headers.set(
2622
+ "vary",
2623
+ requestedHeaders === null ? "Origin" : "Origin, Access-Control-Request-Headers"
2624
+ );
2625
+ return new Response(response.body, { status: response.status, headers });
2626
+ }
2627
+ function publicText(text, contentType) {
2628
+ return new Response(text, {
2629
+ headers: {
2630
+ "content-type": contentType,
2631
+ "cache-control": "public, max-age=300",
2632
+ "x-content-type-options": "nosniff"
2633
+ }
2634
+ });
2635
+ }
1731
2636
  function methodNotAllowed(allow) {
1732
2637
  return new Response("Method not allowed", { status: 405, headers: { allow } });
1733
2638
  }