@tinycloud/mcp 0.7.4-beta.0 → 0.8.0-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.cjs CHANGED
@@ -23,6 +23,7 @@ __export(http_exports, {
23
23
  createHostedMcpApp: () => createHostedMcpApp
24
24
  });
25
25
  module.exports = __toCommonJS(http_exports);
26
+ var import_node_path3 = require("path");
26
27
  var import_server4 = require("@modelcontextprotocol/server");
27
28
  var import_node_sdk3 = require("@tinycloud/node-sdk");
28
29
 
@@ -356,7 +357,7 @@ var RemoteTenantStore = class {
356
357
  async approvalRedirect(state) {
357
358
  const pending = await this.#readPending(state, false);
358
359
  const tenant = await this.#readTenant(pending.tenantId);
359
- const callback = new URL("/connect/callback", this.#config.publicUrl);
360
+ const callback = new URL(`${this.#config.approvalPath ?? "/connect"}/callback`, this.#config.publicUrl);
360
361
  callback.searchParams.set("state", state);
361
362
  const params = new URLSearchParams({
362
363
  did: tenant.sessionDid,
@@ -775,7 +776,7 @@ var RemoteTenantStore = class {
775
776
  await (0, import_promises.rename)(`${path}.processing`, path);
776
777
  }
777
778
  #connectUrl(pending) {
778
- const url = new URL("/connect", this.#config.publicUrl);
779
+ const url = new URL(this.#config.approvalPath ?? "/connect", this.#config.publicUrl);
779
780
  url.searchParams.set("state", `${pending.tenantId}.${pending.nonce}.${this.#signState(pending.tenantId, pending.nonce)}`);
780
781
  return url.toString();
781
782
  }
@@ -954,7 +955,7 @@ function toMcpToolResult(result, bindings = [], options = {}) {
954
955
  let serialized;
955
956
  if (options.surface === "hosted") {
956
957
  serialized = JSON.stringify(canonical);
957
- if (options.readOnly === true && serialized.length > 1e5) {
958
+ if ((options.readOnly === true || options.operationId === "tinycloud.apps.context" || options.operationId === "tinycloud.apps.query") && serialized.length > 1e5) {
958
959
  const sql = options.operationId?.startsWith("tinycloud.sql.") || options.operationId === "tinycloud.apps.query";
959
960
  canonical = {
960
961
  status: "error",
@@ -1008,6 +1009,44 @@ function resultSummary(status) {
1008
1009
  }
1009
1010
  }
1010
1011
 
1012
+ // src/schemas.ts
1013
+ var DRAFT_7 = "http://json-schema.org/draft-07/schema#";
1014
+ var DRAFT_2020_12 = "https://json-schema.org/draft/2020-12/schema";
1015
+ var schemaMaps = /* @__PURE__ */ new Set(["properties", "patternProperties", "definitions", "$defs"]);
1016
+ var schemaArrays = /* @__PURE__ */ new Set(["allOf", "anyOf", "oneOf"]);
1017
+ var schemaValues = /* @__PURE__ */ new Set([
1018
+ "additionalProperties",
1019
+ "items",
1020
+ "contains",
1021
+ "propertyNames",
1022
+ "not",
1023
+ "if",
1024
+ "then",
1025
+ "else"
1026
+ ]);
1027
+ function toMcpJsonSchema(schema) {
1028
+ if (typeof schema === "boolean") return schema;
1029
+ if (Array.isArray(schema.items) || "additionalItems" in schema || "dependencies" in schema || "$ref" in schema && Object.keys(schema).some((key) => key !== "$ref")) {
1030
+ throw new Error("The operations schema needs a semantic migration to JSON Schema 2020-12.");
1031
+ }
1032
+ const projected = {};
1033
+ for (const [key, value] of Object.entries(schema)) {
1034
+ if (key === "$schema") {
1035
+ if (value !== DRAFT_7) throw new Error(`Unsupported operations schema dialect: ${String(value)}.`);
1036
+ projected[key] = DRAFT_2020_12;
1037
+ } else if (schemaMaps.has(key)) {
1038
+ projected[key] = Object.fromEntries(Object.entries(value).map(([name, child]) => [name, toMcpJsonSchema(child)]));
1039
+ } else if (schemaArrays.has(key)) {
1040
+ projected[key] = value.map(toMcpJsonSchema);
1041
+ } else if (schemaValues.has(key)) {
1042
+ projected[key] = toMcpJsonSchema(value);
1043
+ } else {
1044
+ projected[key] = value;
1045
+ }
1046
+ }
1047
+ return projected;
1048
+ }
1049
+
1011
1050
  // src/tools.ts
1012
1051
  var import_meta = {};
1013
1052
  function invocationTargetForMcp(selection) {
@@ -1026,9 +1065,9 @@ var generatedCatalog = packageRequire(
1026
1065
  var stableErrorCodes = Object.fromEntries(
1027
1066
  generatedCatalog.stableErrors.map((code) => [code, true])
1028
1067
  );
1029
- var ALL_SURFACES = ["local", "hosted"];
1068
+ var PRIVATE_SURFACES = ["local", "hosted"];
1030
1069
  function bindingSurfaces(binding) {
1031
- return binding.surfaces ?? ALL_SURFACES;
1070
+ return binding.surfaces ?? PRIVATE_SURFACES;
1032
1071
  }
1033
1072
  var TOOL_BINDINGS = [
1034
1073
  {
@@ -1212,11 +1251,11 @@ var TOOL_BINDINGS = [
1212
1251
  name: "tinycloud_apps_context",
1213
1252
  operationId: "tinycloud.apps.context",
1214
1253
  operationVersion: 1,
1215
- readOnlyHint: true,
1254
+ readOnlyHint: false,
1216
1255
  idempotentHint: true,
1217
- destructiveHint: false,
1218
- openWorldHint: true,
1219
- surfaces: ["hosted"]
1256
+ destructiveHint: true,
1257
+ openWorldHint: false,
1258
+ surfaces: ["hosted", "public"]
1220
1259
  },
1221
1260
  {
1222
1261
  name: "tinycloud_apps_read",
@@ -1225,8 +1264,8 @@ var TOOL_BINDINGS = [
1225
1264
  readOnlyHint: true,
1226
1265
  idempotentHint: true,
1227
1266
  destructiveHint: false,
1228
- openWorldHint: true,
1229
- surfaces: ["hosted"]
1267
+ openWorldHint: false,
1268
+ surfaces: ["hosted", "public"]
1230
1269
  },
1231
1270
  {
1232
1271
  name: "tinycloud_apps_write",
@@ -1235,34 +1274,34 @@ var TOOL_BINDINGS = [
1235
1274
  readOnlyHint: false,
1236
1275
  idempotentHint: false,
1237
1276
  destructiveHint: true,
1238
- openWorldHint: true,
1239
- surfaces: ["hosted"]
1277
+ openWorldHint: false,
1278
+ surfaces: ["hosted", "public"]
1240
1279
  },
1241
1280
  {
1242
1281
  name: "tinycloud_apps_query",
1243
1282
  operationId: "tinycloud.apps.query",
1244
1283
  operationVersion: 1,
1245
- readOnlyHint: true,
1284
+ readOnlyHint: false,
1246
1285
  idempotentHint: true,
1247
- destructiveHint: false,
1248
- openWorldHint: true,
1249
- surfaces: ["hosted"]
1286
+ destructiveHint: true,
1287
+ openWorldHint: false,
1288
+ surfaces: ["hosted", "public"]
1250
1289
  }
1251
1290
  ];
1291
+ var definitions = new Map(generatedCatalog.operations.map((operation) => [
1292
+ `${operation.id}@${operation.version}`,
1293
+ {
1294
+ ...operation,
1295
+ input: toMcpJsonSchema(operation.input),
1296
+ result: toMcpJsonSchema(operation.result)
1297
+ }
1298
+ ]));
1252
1299
  function createJsonSchemaValidator() {
1253
- const ajv = new import_ajv.Ajv({ strict: false });
1254
- (0, import_ajv.addFormats)(ajv);
1255
- return new import_ajv.AjvJsonSchemaValidator(ajv);
1300
+ return new import_ajv.AjvJsonSchemaValidator();
1256
1301
  }
1257
1302
  function registerTinyCloudTools(server, selection, validator) {
1258
- const definitions = new Map(
1259
- generatedCatalog.operations.map((operation) => [
1260
- `${operation.id}@${operation.version}`,
1261
- operation
1262
- ])
1263
- );
1264
- for (const binding of TOOL_BINDINGS) {
1265
- if (!bindingSurfaces(binding).includes(selection.surface)) continue;
1303
+ const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface));
1304
+ for (const binding of bindings) {
1266
1305
  const operation = definitions.get(`${binding.operationId}@${binding.operationVersion}`);
1267
1306
  if (operation === void 0) {
1268
1307
  throw new Error(`Generated operations catalog is missing ${binding.operationId}@${binding.operationVersion}.`);
@@ -1308,15 +1347,15 @@ function registerTinyCloudTools(server, selection, validator) {
1308
1347
  operationVersion: binding.operationVersion,
1309
1348
  input
1310
1349
  });
1311
- const response = toMcpToolResult(projected, TOOL_BINDINGS, {
1312
- surface: selection.surface,
1350
+ const response = toMcpToolResult(projected, bindings, {
1351
+ surface: selection.surface === "local" ? "local" : "hosted",
1313
1352
  readOnly: binding.readOnlyHint,
1314
1353
  operationId: binding.operationId
1315
1354
  });
1316
1355
  code = outcomeCode(response.structuredContent);
1317
1356
  return response;
1318
1357
  } finally {
1319
- if (selection.surface === "hosted") {
1358
+ if (selection.surface !== "local") {
1320
1359
  logToolCall(binding.name, code, startedAt, metrics.replayedGrants);
1321
1360
  }
1322
1361
  }
@@ -1339,7 +1378,7 @@ function outcomeCode(result) {
1339
1378
  // package.json
1340
1379
  var package_default = {
1341
1380
  name: "@tinycloud/mcp",
1342
- version: "0.7.4-beta.0",
1381
+ version: "0.8.0-beta.2",
1343
1382
  description: "TinyCloud delegated operations MCP server",
1344
1383
  keywords: [
1345
1384
  "tinycloud",
@@ -1410,9 +1449,9 @@ var package_default = {
1410
1449
  dependencies: {
1411
1450
  "@modelcontextprotocol/node": "2.0.0-beta.5",
1412
1451
  "@modelcontextprotocol/server": "2.0.0-beta.5",
1413
- "@tinycloud/node-sdk": "3.6.0-beta.0",
1452
+ "@tinycloud/node-sdk": "3.6.0-beta.2",
1414
1453
  "@tinycloud/node-sdk-wasm": "1.7.8",
1415
- "@tinycloud/operations": "0.7.4-beta.0",
1454
+ "@tinycloud/operations": "0.7.4-beta.2",
1416
1455
  jose: "6.2.4"
1417
1456
  },
1418
1457
  devDependencies: {
@@ -1438,6 +1477,29 @@ var MCP_SERVER_NAME = "tinycloud-mcp";
1438
1477
  var REQUIRED_SCOPE = "tinycloud:mcp";
1439
1478
  function createHostedMcpApp(config) {
1440
1479
  const resourceUrl = canonicalResourceUrl(config.publicUrl);
1480
+ const pluginUrl = new URL("./chatgpt/mcp", resourceUrl);
1481
+ const hosted = createHostedSurface(config, resourceUrl, "hosted");
1482
+ const plugin = createHostedSurface({
1483
+ ...config,
1484
+ stateDir: (0, import_node_path3.join)(config.stateDir, "chatgpt"),
1485
+ approvalPath: new URL("./connect", pluginUrl).pathname
1486
+ }, pluginUrl, "public");
1487
+ const pluginPaths = {
1488
+ [pluginUrl.pathname]: true,
1489
+ [new URL("./connect", pluginUrl).pathname]: true,
1490
+ [new URL("./connect/callback", pluginUrl).pathname]: true,
1491
+ [new URL((0, import_server4.getOAuthProtectedResourceMetadataUrl)(pluginUrl)).pathname]: true
1492
+ };
1493
+ return {
1494
+ fetch: (request) => (pluginPaths[new URL(request.url).pathname] === true ? plugin : hosted).fetch(request),
1495
+ close: async () => {
1496
+ await Promise.all([hosted.close(), plugin.close()]);
1497
+ }
1498
+ };
1499
+ }
1500
+ function createHostedSurface(config, resourceUrl, surface) {
1501
+ const connectPath = config.approvalPath ?? "/connect";
1502
+ const callbackPath = `${connectPath}/callback`;
1441
1503
  const issuer = String(config.oauthMetadata.issuer);
1442
1504
  const jwksUri = String(config.oauthMetadata.jwks_uri);
1443
1505
  const verifier = createOpenKeyTokenVerifier({
@@ -1462,11 +1524,11 @@ function createHostedMcpApp(config) {
1462
1524
  { name: MCP_SERVER_NAME, version: MCP_VERSION },
1463
1525
  {
1464
1526
  jsonSchemaValidator: validator,
1465
- instructions: "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."
1527
+ 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."
1466
1528
  }
1467
1529
  );
1468
1530
  registerTinyCloudTools(server, {
1469
- surface: "hosted",
1531
+ surface,
1470
1532
  profile: "agent",
1471
1533
  explicitProfile: true,
1472
1534
  allowOwnerProfile: false,
@@ -1474,7 +1536,7 @@ function createHostedMcpApp(config) {
1474
1536
  beforeOperation: () => store.validateTenant(subject, ownerDids),
1475
1537
  transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
1476
1538
  }, validator);
1477
- registerConnectTool(server, validator, store, identity);
1539
+ registerConnectTool(server, validator, store, identity, surface);
1478
1540
  return server;
1479
1541
  }, {
1480
1542
  legacy: "stateless",
@@ -1485,7 +1547,7 @@ function createHostedMcpApp(config) {
1485
1547
  resourceServerUrl: resourceUrl,
1486
1548
  serviceDocumentationUrl: new URL("https://docs.tinycloud.xyz/cli/mcp"),
1487
1549
  scopesSupported: [REQUIRED_SCOPE],
1488
- resourceName: "TinyCloud hosted MCP"
1550
+ resourceName: surface === "public" ? "TinyCloud personal records" : "TinyCloud hosted MCP"
1489
1551
  };
1490
1552
  const allowedHosts = [resourceUrl.hostname];
1491
1553
  return {
@@ -1498,16 +1560,32 @@ function createHostedMcpApp(config) {
1498
1560
  }
1499
1561
  const hostFailure = (0, import_server4.hostHeaderValidationResponse)(request, allowedHosts);
1500
1562
  if (hostFailure !== void 0) return hostFailure;
1501
- if (url.pathname === "/connect") {
1563
+ if (url.pathname === connectPath) {
1502
1564
  if (request.method !== "GET") return methodNotAllowed("GET");
1503
1565
  try {
1504
1566
  const destination = await store.approvalRedirect(requiredState(url));
1505
1567
  return Response.redirect(destination, 302);
1506
1568
  } catch {
1569
+ if (request.headers.get("accept")?.includes("text/html")) {
1570
+ return new Response(
1571
+ '<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>TinyCloud approval unavailable</title><main><h1>This approval link is unavailable</h1><p>It may have expired or already been used. No new approval was accepted through this link.</p><p>Return to the conversation that requested access. In ChatGPT, select @TinyCloud and ask it to continue your original request. If access is still needed, it can provide a fresh approval link.</p><p>If you declined access, no further approval is required. Never paste passwords, codes or approval links into support messages.</p></main></html>',
1572
+ {
1573
+ status: 400,
1574
+ headers: {
1575
+ "content-type": "text/html; charset=utf-8",
1576
+ "cache-control": "no-store",
1577
+ "content-security-policy": "default-src 'none'; base-uri 'none'; frame-ancestors 'none'",
1578
+ "referrer-policy": "no-referrer",
1579
+ "x-content-type-options": "nosniff",
1580
+ vary: "Accept"
1581
+ }
1582
+ }
1583
+ );
1584
+ }
1507
1585
  return json({ error: "The approval link is invalid or expired." }, 400);
1508
1586
  }
1509
1587
  }
1510
- if (url.pathname === "/connect/callback") {
1588
+ if (url.pathname === callbackPath) {
1511
1589
  if (request.method === "OPTIONS") return callbackCors(new Response(null, { status: 204 }), config.openkeyHost);
1512
1590
  if (request.method !== "POST") return methodNotAllowed("POST");
1513
1591
  const originFailure2 = validateCallbackOrigin(request, config.openkeyHost);
@@ -1547,17 +1625,17 @@ function createHostedMcpApp(config) {
1547
1625
  close: () => handler.close()
1548
1626
  };
1549
1627
  }
1550
- function registerConnectTool(server, validator, store, identity) {
1628
+ function registerConnectTool(server, validator, store, identity, surface) {
1551
1629
  const inputSchema = (0, import_server4.fromJsonSchema)({ type: "object", properties: {}, additionalProperties: false }, validator);
1552
1630
  const outputSchema = (0, import_server4.fromJsonSchema)({
1553
1631
  type: "object",
1554
1632
  properties: {
1555
1633
  connected: { type: "boolean" },
1556
- sessionDid: { type: "string" },
1634
+ ...surface === "hosted" ? { sessionDid: { type: "string" } } : {},
1557
1635
  approvalUrl: { type: "string", format: "uri" },
1558
1636
  expiresAt: { type: "string", format: "date-time" }
1559
1637
  },
1560
- required: ["connected", "sessionDid"],
1638
+ required: surface === "hosted" ? ["connected", "sessionDid"] : ["connected"],
1561
1639
  additionalProperties: false
1562
1640
  }, validator);
1563
1641
  server.registerTool(
@@ -1571,7 +1649,7 @@ function registerConnectTool(server, validator, store, identity) {
1571
1649
  readOnlyHint: false,
1572
1650
  idempotentHint: true,
1573
1651
  destructiveHint: false,
1574
- openWorldHint: true
1652
+ openWorldHint: false
1575
1653
  }
1576
1654
  },
1577
1655
  async () => {
@@ -1580,12 +1658,17 @@ function registerConnectTool(server, validator, store, identity) {
1580
1658
  try {
1581
1659
  const status = await store.connectStatus(identity.subject, identity.ownerDids);
1582
1660
  code = status.connected ? "ok" : "authority_required";
1661
+ const visibleStatus = surface === "hosted" ? status : {
1662
+ connected: status.connected,
1663
+ ...status.approvalUrl === void 0 ? {} : { approvalUrl: status.approvalUrl },
1664
+ ...status.expiresAt === void 0 ? {} : { expiresAt: status.expiresAt }
1665
+ };
1583
1666
  return {
1584
1667
  content: [{
1585
1668
  type: "text",
1586
1669
  text: status.connected ? "TinyCloud is connected." : `TinyCloud approval is required. Open ${status.approvalUrl}, then call this tool again. This link expires at ${status.expiresAt}.`
1587
1670
  }],
1588
- structuredContent: status
1671
+ structuredContent: visibleStatus
1589
1672
  };
1590
1673
  } finally {
1591
1674
  logToolCall("tinycloud_connect", code, startedAt, 0);