@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-cli.cjs CHANGED
@@ -24,10 +24,11 @@ __export(http_cli_exports, {
24
24
  });
25
25
  module.exports = __toCommonJS(http_cli_exports);
26
26
  var import_node_http = require("http");
27
- var import_node_path3 = require("path");
27
+ var import_node_path4 = require("path");
28
28
  var import_node = require("@modelcontextprotocol/node");
29
29
 
30
30
  // src/http.ts
31
+ var import_node_path3 = require("path");
31
32
  var import_server4 = require("@modelcontextprotocol/server");
32
33
  var import_node_sdk3 = require("@tinycloud/node-sdk");
33
34
 
@@ -373,7 +374,7 @@ var RemoteTenantStore = class {
373
374
  async approvalRedirect(state) {
374
375
  const pending = await this.#readPending(state, false);
375
376
  const tenant = await this.#readTenant(pending.tenantId);
376
- const callback = new URL("/connect/callback", this.#config.publicUrl);
377
+ const callback = new URL(`${this.#config.approvalPath ?? "/connect"}/callback`, this.#config.publicUrl);
377
378
  callback.searchParams.set("state", state);
378
379
  const params = new URLSearchParams({
379
380
  did: tenant.sessionDid,
@@ -792,7 +793,7 @@ var RemoteTenantStore = class {
792
793
  await (0, import_promises.rename)(`${path}.processing`, path);
793
794
  }
794
795
  #connectUrl(pending) {
795
- const url = new URL("/connect", this.#config.publicUrl);
796
+ const url = new URL(this.#config.approvalPath ?? "/connect", this.#config.publicUrl);
796
797
  url.searchParams.set("state", `${pending.tenantId}.${pending.nonce}.${this.#signState(pending.tenantId, pending.nonce)}`);
797
798
  return url.toString();
798
799
  }
@@ -971,7 +972,7 @@ function toMcpToolResult(result, bindings = [], options = {}) {
971
972
  let serialized;
972
973
  if (options.surface === "hosted") {
973
974
  serialized = JSON.stringify(canonical);
974
- if (options.readOnly === true && serialized.length > 1e5) {
975
+ if ((options.readOnly === true || options.operationId === "tinycloud.apps.context" || options.operationId === "tinycloud.apps.query") && serialized.length > 1e5) {
975
976
  const sql = options.operationId?.startsWith("tinycloud.sql.") || options.operationId === "tinycloud.apps.query";
976
977
  canonical = {
977
978
  status: "error",
@@ -1025,6 +1026,44 @@ function resultSummary(status) {
1025
1026
  }
1026
1027
  }
1027
1028
 
1029
+ // src/schemas.ts
1030
+ var DRAFT_7 = "http://json-schema.org/draft-07/schema#";
1031
+ var DRAFT_2020_12 = "https://json-schema.org/draft/2020-12/schema";
1032
+ var schemaMaps = /* @__PURE__ */ new Set(["properties", "patternProperties", "definitions", "$defs"]);
1033
+ var schemaArrays = /* @__PURE__ */ new Set(["allOf", "anyOf", "oneOf"]);
1034
+ var schemaValues = /* @__PURE__ */ new Set([
1035
+ "additionalProperties",
1036
+ "items",
1037
+ "contains",
1038
+ "propertyNames",
1039
+ "not",
1040
+ "if",
1041
+ "then",
1042
+ "else"
1043
+ ]);
1044
+ function toMcpJsonSchema(schema) {
1045
+ if (typeof schema === "boolean") return schema;
1046
+ if (Array.isArray(schema.items) || "additionalItems" in schema || "dependencies" in schema || "$ref" in schema && Object.keys(schema).some((key) => key !== "$ref")) {
1047
+ throw new Error("The operations schema needs a semantic migration to JSON Schema 2020-12.");
1048
+ }
1049
+ const projected = {};
1050
+ for (const [key, value] of Object.entries(schema)) {
1051
+ if (key === "$schema") {
1052
+ if (value !== DRAFT_7) throw new Error(`Unsupported operations schema dialect: ${String(value)}.`);
1053
+ projected[key] = DRAFT_2020_12;
1054
+ } else if (schemaMaps.has(key)) {
1055
+ projected[key] = Object.fromEntries(Object.entries(value).map(([name, child]) => [name, toMcpJsonSchema(child)]));
1056
+ } else if (schemaArrays.has(key)) {
1057
+ projected[key] = value.map(toMcpJsonSchema);
1058
+ } else if (schemaValues.has(key)) {
1059
+ projected[key] = toMcpJsonSchema(value);
1060
+ } else {
1061
+ projected[key] = value;
1062
+ }
1063
+ }
1064
+ return projected;
1065
+ }
1066
+
1028
1067
  // src/tools.ts
1029
1068
  var import_meta = {};
1030
1069
  function invocationTargetForMcp(selection) {
@@ -1043,9 +1082,9 @@ var generatedCatalog = packageRequire(
1043
1082
  var stableErrorCodes = Object.fromEntries(
1044
1083
  generatedCatalog.stableErrors.map((code) => [code, true])
1045
1084
  );
1046
- var ALL_SURFACES = ["local", "hosted"];
1085
+ var PRIVATE_SURFACES = ["local", "hosted"];
1047
1086
  function bindingSurfaces(binding) {
1048
- return binding.surfaces ?? ALL_SURFACES;
1087
+ return binding.surfaces ?? PRIVATE_SURFACES;
1049
1088
  }
1050
1089
  var TOOL_BINDINGS = [
1051
1090
  {
@@ -1229,11 +1268,11 @@ var TOOL_BINDINGS = [
1229
1268
  name: "tinycloud_apps_context",
1230
1269
  operationId: "tinycloud.apps.context",
1231
1270
  operationVersion: 1,
1232
- readOnlyHint: true,
1271
+ readOnlyHint: false,
1233
1272
  idempotentHint: true,
1234
- destructiveHint: false,
1235
- openWorldHint: true,
1236
- surfaces: ["hosted"]
1273
+ destructiveHint: true,
1274
+ openWorldHint: false,
1275
+ surfaces: ["hosted", "public"]
1237
1276
  },
1238
1277
  {
1239
1278
  name: "tinycloud_apps_read",
@@ -1242,8 +1281,8 @@ var TOOL_BINDINGS = [
1242
1281
  readOnlyHint: true,
1243
1282
  idempotentHint: true,
1244
1283
  destructiveHint: false,
1245
- openWorldHint: true,
1246
- surfaces: ["hosted"]
1284
+ openWorldHint: false,
1285
+ surfaces: ["hosted", "public"]
1247
1286
  },
1248
1287
  {
1249
1288
  name: "tinycloud_apps_write",
@@ -1252,34 +1291,34 @@ var TOOL_BINDINGS = [
1252
1291
  readOnlyHint: false,
1253
1292
  idempotentHint: false,
1254
1293
  destructiveHint: true,
1255
- openWorldHint: true,
1256
- surfaces: ["hosted"]
1294
+ openWorldHint: false,
1295
+ surfaces: ["hosted", "public"]
1257
1296
  },
1258
1297
  {
1259
1298
  name: "tinycloud_apps_query",
1260
1299
  operationId: "tinycloud.apps.query",
1261
1300
  operationVersion: 1,
1262
- readOnlyHint: true,
1301
+ readOnlyHint: false,
1263
1302
  idempotentHint: true,
1264
- destructiveHint: false,
1265
- openWorldHint: true,
1266
- surfaces: ["hosted"]
1303
+ destructiveHint: true,
1304
+ openWorldHint: false,
1305
+ surfaces: ["hosted", "public"]
1267
1306
  }
1268
1307
  ];
1308
+ var definitions = new Map(generatedCatalog.operations.map((operation) => [
1309
+ `${operation.id}@${operation.version}`,
1310
+ {
1311
+ ...operation,
1312
+ input: toMcpJsonSchema(operation.input),
1313
+ result: toMcpJsonSchema(operation.result)
1314
+ }
1315
+ ]));
1269
1316
  function createJsonSchemaValidator() {
1270
- const ajv = new import_ajv.Ajv({ strict: false });
1271
- (0, import_ajv.addFormats)(ajv);
1272
- return new import_ajv.AjvJsonSchemaValidator(ajv);
1317
+ return new import_ajv.AjvJsonSchemaValidator();
1273
1318
  }
1274
1319
  function registerTinyCloudTools(server, selection, validator) {
1275
- const definitions = new Map(
1276
- generatedCatalog.operations.map((operation) => [
1277
- `${operation.id}@${operation.version}`,
1278
- operation
1279
- ])
1280
- );
1281
- for (const binding of TOOL_BINDINGS) {
1282
- if (!bindingSurfaces(binding).includes(selection.surface)) continue;
1320
+ const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface));
1321
+ for (const binding of bindings) {
1283
1322
  const operation = definitions.get(`${binding.operationId}@${binding.operationVersion}`);
1284
1323
  if (operation === void 0) {
1285
1324
  throw new Error(`Generated operations catalog is missing ${binding.operationId}@${binding.operationVersion}.`);
@@ -1325,15 +1364,15 @@ function registerTinyCloudTools(server, selection, validator) {
1325
1364
  operationVersion: binding.operationVersion,
1326
1365
  input
1327
1366
  });
1328
- const response = toMcpToolResult(projected, TOOL_BINDINGS, {
1329
- surface: selection.surface,
1367
+ const response = toMcpToolResult(projected, bindings, {
1368
+ surface: selection.surface === "local" ? "local" : "hosted",
1330
1369
  readOnly: binding.readOnlyHint,
1331
1370
  operationId: binding.operationId
1332
1371
  });
1333
1372
  code = outcomeCode(response.structuredContent);
1334
1373
  return response;
1335
1374
  } finally {
1336
- if (selection.surface === "hosted") {
1375
+ if (selection.surface !== "local") {
1337
1376
  logToolCall(binding.name, code, startedAt, metrics.replayedGrants);
1338
1377
  }
1339
1378
  }
@@ -1356,7 +1395,7 @@ function outcomeCode(result) {
1356
1395
  // package.json
1357
1396
  var package_default = {
1358
1397
  name: "@tinycloud/mcp",
1359
- version: "0.7.4-beta.0",
1398
+ version: "0.8.0-beta.2",
1360
1399
  description: "TinyCloud delegated operations MCP server",
1361
1400
  keywords: [
1362
1401
  "tinycloud",
@@ -1427,9 +1466,9 @@ var package_default = {
1427
1466
  dependencies: {
1428
1467
  "@modelcontextprotocol/node": "2.0.0-beta.5",
1429
1468
  "@modelcontextprotocol/server": "2.0.0-beta.5",
1430
- "@tinycloud/node-sdk": "3.6.0-beta.0",
1469
+ "@tinycloud/node-sdk": "3.6.0-beta.2",
1431
1470
  "@tinycloud/node-sdk-wasm": "1.7.8",
1432
- "@tinycloud/operations": "0.7.4-beta.0",
1471
+ "@tinycloud/operations": "0.7.4-beta.2",
1433
1472
  jose: "6.2.4"
1434
1473
  },
1435
1474
  devDependencies: {
@@ -1455,6 +1494,29 @@ var MCP_SERVER_NAME = "tinycloud-mcp";
1455
1494
  var REQUIRED_SCOPE = "tinycloud:mcp";
1456
1495
  function createHostedMcpApp(config) {
1457
1496
  const resourceUrl = canonicalResourceUrl(config.publicUrl);
1497
+ const pluginUrl = new URL("./chatgpt/mcp", resourceUrl);
1498
+ const hosted = createHostedSurface(config, resourceUrl, "hosted");
1499
+ const plugin = createHostedSurface({
1500
+ ...config,
1501
+ stateDir: (0, import_node_path3.join)(config.stateDir, "chatgpt"),
1502
+ approvalPath: new URL("./connect", pluginUrl).pathname
1503
+ }, pluginUrl, "public");
1504
+ const pluginPaths = {
1505
+ [pluginUrl.pathname]: true,
1506
+ [new URL("./connect", pluginUrl).pathname]: true,
1507
+ [new URL("./connect/callback", pluginUrl).pathname]: true,
1508
+ [new URL((0, import_server4.getOAuthProtectedResourceMetadataUrl)(pluginUrl)).pathname]: true
1509
+ };
1510
+ return {
1511
+ fetch: (request) => (pluginPaths[new URL(request.url).pathname] === true ? plugin : hosted).fetch(request),
1512
+ close: async () => {
1513
+ await Promise.all([hosted.close(), plugin.close()]);
1514
+ }
1515
+ };
1516
+ }
1517
+ function createHostedSurface(config, resourceUrl, surface) {
1518
+ const connectPath = config.approvalPath ?? "/connect";
1519
+ const callbackPath = `${connectPath}/callback`;
1458
1520
  const issuer = String(config.oauthMetadata.issuer);
1459
1521
  const jwksUri = String(config.oauthMetadata.jwks_uri);
1460
1522
  const verifier = createOpenKeyTokenVerifier({
@@ -1479,11 +1541,11 @@ function createHostedMcpApp(config) {
1479
1541
  { name: MCP_SERVER_NAME, version: MCP_VERSION },
1480
1542
  {
1481
1543
  jsonSchemaValidator: validator,
1482
- 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."
1544
+ 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."
1483
1545
  }
1484
1546
  );
1485
1547
  registerTinyCloudTools(server, {
1486
- surface: "hosted",
1548
+ surface,
1487
1549
  profile: "agent",
1488
1550
  explicitProfile: true,
1489
1551
  allowOwnerProfile: false,
@@ -1491,7 +1553,7 @@ function createHostedMcpApp(config) {
1491
1553
  beforeOperation: () => store.validateTenant(subject, ownerDids),
1492
1554
  transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
1493
1555
  }, validator);
1494
- registerConnectTool(server, validator, store, identity);
1556
+ registerConnectTool(server, validator, store, identity, surface);
1495
1557
  return server;
1496
1558
  }, {
1497
1559
  legacy: "stateless",
@@ -1502,7 +1564,7 @@ function createHostedMcpApp(config) {
1502
1564
  resourceServerUrl: resourceUrl,
1503
1565
  serviceDocumentationUrl: new URL("https://docs.tinycloud.xyz/cli/mcp"),
1504
1566
  scopesSupported: [REQUIRED_SCOPE],
1505
- resourceName: "TinyCloud hosted MCP"
1567
+ resourceName: surface === "public" ? "TinyCloud personal records" : "TinyCloud hosted MCP"
1506
1568
  };
1507
1569
  const allowedHosts = [resourceUrl.hostname];
1508
1570
  return {
@@ -1515,16 +1577,32 @@ function createHostedMcpApp(config) {
1515
1577
  }
1516
1578
  const hostFailure = (0, import_server4.hostHeaderValidationResponse)(request, allowedHosts);
1517
1579
  if (hostFailure !== void 0) return hostFailure;
1518
- if (url.pathname === "/connect") {
1580
+ if (url.pathname === connectPath) {
1519
1581
  if (request.method !== "GET") return methodNotAllowed("GET");
1520
1582
  try {
1521
1583
  const destination = await store.approvalRedirect(requiredState(url));
1522
1584
  return Response.redirect(destination, 302);
1523
1585
  } catch {
1586
+ if (request.headers.get("accept")?.includes("text/html")) {
1587
+ return new Response(
1588
+ '<!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>',
1589
+ {
1590
+ status: 400,
1591
+ headers: {
1592
+ "content-type": "text/html; charset=utf-8",
1593
+ "cache-control": "no-store",
1594
+ "content-security-policy": "default-src 'none'; base-uri 'none'; frame-ancestors 'none'",
1595
+ "referrer-policy": "no-referrer",
1596
+ "x-content-type-options": "nosniff",
1597
+ vary: "Accept"
1598
+ }
1599
+ }
1600
+ );
1601
+ }
1524
1602
  return json({ error: "The approval link is invalid or expired." }, 400);
1525
1603
  }
1526
1604
  }
1527
- if (url.pathname === "/connect/callback") {
1605
+ if (url.pathname === callbackPath) {
1528
1606
  if (request.method === "OPTIONS") return callbackCors(new Response(null, { status: 204 }), config.openkeyHost);
1529
1607
  if (request.method !== "POST") return methodNotAllowed("POST");
1530
1608
  const originFailure2 = validateCallbackOrigin(request, config.openkeyHost);
@@ -1564,17 +1642,17 @@ function createHostedMcpApp(config) {
1564
1642
  close: () => handler.close()
1565
1643
  };
1566
1644
  }
1567
- function registerConnectTool(server, validator, store, identity) {
1645
+ function registerConnectTool(server, validator, store, identity, surface) {
1568
1646
  const inputSchema = (0, import_server4.fromJsonSchema)({ type: "object", properties: {}, additionalProperties: false }, validator);
1569
1647
  const outputSchema = (0, import_server4.fromJsonSchema)({
1570
1648
  type: "object",
1571
1649
  properties: {
1572
1650
  connected: { type: "boolean" },
1573
- sessionDid: { type: "string" },
1651
+ ...surface === "hosted" ? { sessionDid: { type: "string" } } : {},
1574
1652
  approvalUrl: { type: "string", format: "uri" },
1575
1653
  expiresAt: { type: "string", format: "date-time" }
1576
1654
  },
1577
- required: ["connected", "sessionDid"],
1655
+ required: surface === "hosted" ? ["connected", "sessionDid"] : ["connected"],
1578
1656
  additionalProperties: false
1579
1657
  }, validator);
1580
1658
  server.registerTool(
@@ -1588,7 +1666,7 @@ function registerConnectTool(server, validator, store, identity) {
1588
1666
  readOnlyHint: false,
1589
1667
  idempotentHint: true,
1590
1668
  destructiveHint: false,
1591
- openWorldHint: true
1669
+ openWorldHint: false
1592
1670
  }
1593
1671
  },
1594
1672
  async () => {
@@ -1597,12 +1675,17 @@ function registerConnectTool(server, validator, store, identity) {
1597
1675
  try {
1598
1676
  const status = await store.connectStatus(identity.subject, identity.ownerDids);
1599
1677
  code = status.connected ? "ok" : "authority_required";
1678
+ const visibleStatus = surface === "hosted" ? status : {
1679
+ connected: status.connected,
1680
+ ...status.approvalUrl === void 0 ? {} : { approvalUrl: status.approvalUrl },
1681
+ ...status.expiresAt === void 0 ? {} : { expiresAt: status.expiresAt }
1682
+ };
1600
1683
  return {
1601
1684
  content: [{
1602
1685
  type: "text",
1603
1686
  text: status.connected ? "TinyCloud is connected." : `TinyCloud approval is required. Open ${status.approvalUrl}, then call this tool again. This link expires at ${status.expiresAt}.`
1604
1687
  }],
1605
- structuredContent: status
1688
+ structuredContent: visibleStatus
1606
1689
  };
1607
1690
  } finally {
1608
1691
  logToolCall("tinycloud_connect", code, startedAt, 0);
@@ -1715,8 +1798,8 @@ function requiredUrl(name) {
1715
1798
  }
1716
1799
  function requiredAbsolutePath(name) {
1717
1800
  const value = requiredString(name);
1718
- if (!(0, import_node_path3.isAbsolute)(value)) throw new Error(`${name} must be an absolute path.`);
1719
- return (0, import_node_path3.resolve)(value);
1801
+ if (!(0, import_node_path4.isAbsolute)(value)) throw new Error(`${name} must be an absolute path.`);
1802
+ return (0, import_node_path4.resolve)(value);
1720
1803
  }
1721
1804
  function positiveInteger(value, fallback) {
1722
1805
  if (value === void 0) return fallback;