@tinycloud/mcp 0.7.4-beta.1 → 0.8.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/http-cli.js CHANGED
@@ -4,6 +4,7 @@ import { isAbsolute, resolve } from "path";
4
4
  import { toNodeHandler } from "@modelcontextprotocol/node";
5
5
 
6
6
  // src/http.ts
7
+ import { join as join3 } from "path";
7
8
  import {
8
9
  McpServer as McpServer2,
9
10
  createMcpHandler,
@@ -369,7 +370,7 @@ var RemoteTenantStore = class {
369
370
  async approvalRedirect(state) {
370
371
  const pending = await this.#readPending(state, false);
371
372
  const tenant = await this.#readTenant(pending.tenantId);
372
- const callback = new URL("/connect/callback", this.#config.publicUrl);
373
+ const callback = new URL(`${this.#config.approvalPath ?? "/connect"}/callback`, this.#config.publicUrl);
373
374
  callback.searchParams.set("state", state);
374
375
  const params = new URLSearchParams({
375
376
  did: tenant.sessionDid,
@@ -788,7 +789,7 @@ var RemoteTenantStore = class {
788
789
  await rename(`${path}.processing`, path);
789
790
  }
790
791
  #connectUrl(pending) {
791
- const url = new URL("/connect", this.#config.publicUrl);
792
+ const url = new URL(this.#config.approvalPath ?? "/connect", this.#config.publicUrl);
792
793
  url.searchParams.set("state", `${pending.tenantId}.${pending.nonce}.${this.#signState(pending.tenantId, pending.nonce)}`);
793
794
  return url.toString();
794
795
  }
@@ -953,7 +954,7 @@ import { serveStdio } from "@modelcontextprotocol/server/stdio";
953
954
  import { createRequire } from "module";
954
955
  import { join as join2 } from "path";
955
956
  import { fromJsonSchema } from "@modelcontextprotocol/server";
956
- import { addFormats, Ajv, AjvJsonSchemaValidator } from "@modelcontextprotocol/server/validators/ajv";
957
+ import { AjvJsonSchemaValidator } from "@modelcontextprotocol/server/validators/ajv";
957
958
  import { invokeOperation as invokeOperation2 } from "@tinycloud/operations";
958
959
 
959
960
  // src/results.ts
@@ -967,7 +968,7 @@ function toMcpToolResult(result, bindings = [], options = {}) {
967
968
  let serialized;
968
969
  if (options.surface === "hosted") {
969
970
  serialized = JSON.stringify(canonical);
970
- if (options.readOnly === true && serialized.length > 1e5) {
971
+ if ((options.readOnly === true || options.operationId === "tinycloud.apps.context" || options.operationId === "tinycloud.apps.query") && serialized.length > 1e5) {
971
972
  const sql = options.operationId?.startsWith("tinycloud.sql.") || options.operationId === "tinycloud.apps.query";
972
973
  canonical = {
973
974
  status: "error",
@@ -1021,6 +1022,44 @@ function resultSummary(status) {
1021
1022
  }
1022
1023
  }
1023
1024
 
1025
+ // src/schemas.ts
1026
+ var DRAFT_7 = "http://json-schema.org/draft-07/schema#";
1027
+ var DRAFT_2020_12 = "https://json-schema.org/draft/2020-12/schema";
1028
+ var schemaMaps = /* @__PURE__ */ new Set(["properties", "patternProperties", "definitions", "$defs"]);
1029
+ var schemaArrays = /* @__PURE__ */ new Set(["allOf", "anyOf", "oneOf"]);
1030
+ var schemaValues = /* @__PURE__ */ new Set([
1031
+ "additionalProperties",
1032
+ "items",
1033
+ "contains",
1034
+ "propertyNames",
1035
+ "not",
1036
+ "if",
1037
+ "then",
1038
+ "else"
1039
+ ]);
1040
+ function toMcpJsonSchema(schema) {
1041
+ if (typeof schema === "boolean") return schema;
1042
+ if (Array.isArray(schema.items) || "additionalItems" in schema || "dependencies" in schema || "$ref" in schema && Object.keys(schema).some((key) => key !== "$ref")) {
1043
+ throw new Error("The operations schema needs a semantic migration to JSON Schema 2020-12.");
1044
+ }
1045
+ const projected = {};
1046
+ for (const [key, value] of Object.entries(schema)) {
1047
+ if (key === "$schema") {
1048
+ if (value !== DRAFT_7) throw new Error(`Unsupported operations schema dialect: ${String(value)}.`);
1049
+ projected[key] = DRAFT_2020_12;
1050
+ } else if (schemaMaps.has(key)) {
1051
+ projected[key] = Object.fromEntries(Object.entries(value).map(([name, child]) => [name, toMcpJsonSchema(child)]));
1052
+ } else if (schemaArrays.has(key)) {
1053
+ projected[key] = value.map(toMcpJsonSchema);
1054
+ } else if (schemaValues.has(key)) {
1055
+ projected[key] = toMcpJsonSchema(value);
1056
+ } else {
1057
+ projected[key] = value;
1058
+ }
1059
+ }
1060
+ return projected;
1061
+ }
1062
+
1024
1063
  // src/tools.ts
1025
1064
  function invocationTargetForMcp(selection) {
1026
1065
  return {
@@ -1038,9 +1077,9 @@ var generatedCatalog = packageRequire(
1038
1077
  var stableErrorCodes = Object.fromEntries(
1039
1078
  generatedCatalog.stableErrors.map((code) => [code, true])
1040
1079
  );
1041
- var ALL_SURFACES = ["local", "hosted"];
1080
+ var PRIVATE_SURFACES = ["local", "hosted"];
1042
1081
  function bindingSurfaces(binding) {
1043
- return binding.surfaces ?? ALL_SURFACES;
1082
+ return binding.surfaces ?? PRIVATE_SURFACES;
1044
1083
  }
1045
1084
  var TOOL_BINDINGS = [
1046
1085
  {
@@ -1224,11 +1263,11 @@ var TOOL_BINDINGS = [
1224
1263
  name: "tinycloud_apps_context",
1225
1264
  operationId: "tinycloud.apps.context",
1226
1265
  operationVersion: 1,
1227
- readOnlyHint: true,
1266
+ readOnlyHint: false,
1228
1267
  idempotentHint: true,
1229
- destructiveHint: false,
1230
- openWorldHint: true,
1231
- surfaces: ["hosted"]
1268
+ destructiveHint: true,
1269
+ openWorldHint: false,
1270
+ surfaces: ["hosted", "public"]
1232
1271
  },
1233
1272
  {
1234
1273
  name: "tinycloud_apps_read",
@@ -1237,8 +1276,8 @@ var TOOL_BINDINGS = [
1237
1276
  readOnlyHint: true,
1238
1277
  idempotentHint: true,
1239
1278
  destructiveHint: false,
1240
- openWorldHint: true,
1241
- surfaces: ["hosted"]
1279
+ openWorldHint: false,
1280
+ surfaces: ["hosted", "public"]
1242
1281
  },
1243
1282
  {
1244
1283
  name: "tinycloud_apps_write",
@@ -1247,34 +1286,34 @@ var TOOL_BINDINGS = [
1247
1286
  readOnlyHint: false,
1248
1287
  idempotentHint: false,
1249
1288
  destructiveHint: true,
1250
- openWorldHint: true,
1251
- surfaces: ["hosted"]
1289
+ openWorldHint: false,
1290
+ surfaces: ["hosted", "public"]
1252
1291
  },
1253
1292
  {
1254
1293
  name: "tinycloud_apps_query",
1255
1294
  operationId: "tinycloud.apps.query",
1256
1295
  operationVersion: 1,
1257
- readOnlyHint: true,
1296
+ readOnlyHint: false,
1258
1297
  idempotentHint: true,
1259
- destructiveHint: false,
1260
- openWorldHint: true,
1261
- surfaces: ["hosted"]
1298
+ destructiveHint: true,
1299
+ openWorldHint: false,
1300
+ surfaces: ["hosted", "public"]
1262
1301
  }
1263
1302
  ];
1303
+ var definitions = new Map(generatedCatalog.operations.map((operation) => [
1304
+ `${operation.id}@${operation.version}`,
1305
+ {
1306
+ ...operation,
1307
+ input: toMcpJsonSchema(operation.input),
1308
+ result: toMcpJsonSchema(operation.result)
1309
+ }
1310
+ ]));
1264
1311
  function createJsonSchemaValidator() {
1265
- const ajv = new Ajv({ strict: false });
1266
- addFormats(ajv);
1267
- return new AjvJsonSchemaValidator(ajv);
1312
+ return new AjvJsonSchemaValidator();
1268
1313
  }
1269
1314
  function registerTinyCloudTools(server, selection, validator) {
1270
- const definitions = new Map(
1271
- generatedCatalog.operations.map((operation) => [
1272
- `${operation.id}@${operation.version}`,
1273
- operation
1274
- ])
1275
- );
1276
- for (const binding of TOOL_BINDINGS) {
1277
- if (!bindingSurfaces(binding).includes(selection.surface)) continue;
1315
+ const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface));
1316
+ for (const binding of bindings) {
1278
1317
  const operation = definitions.get(`${binding.operationId}@${binding.operationVersion}`);
1279
1318
  if (operation === void 0) {
1280
1319
  throw new Error(`Generated operations catalog is missing ${binding.operationId}@${binding.operationVersion}.`);
@@ -1320,15 +1359,15 @@ function registerTinyCloudTools(server, selection, validator) {
1320
1359
  operationVersion: binding.operationVersion,
1321
1360
  input
1322
1361
  });
1323
- const response = toMcpToolResult(projected, TOOL_BINDINGS, {
1324
- surface: selection.surface,
1362
+ const response = toMcpToolResult(projected, bindings, {
1363
+ surface: selection.surface === "local" ? "local" : "hosted",
1325
1364
  readOnly: binding.readOnlyHint,
1326
1365
  operationId: binding.operationId
1327
1366
  });
1328
1367
  code = outcomeCode(response.structuredContent);
1329
1368
  return response;
1330
1369
  } finally {
1331
- if (selection.surface === "hosted") {
1370
+ if (selection.surface !== "local") {
1332
1371
  logToolCall(binding.name, code, startedAt, metrics.replayedGrants);
1333
1372
  }
1334
1373
  }
@@ -1351,7 +1390,7 @@ function outcomeCode(result) {
1351
1390
  // package.json
1352
1391
  var package_default = {
1353
1392
  name: "@tinycloud/mcp",
1354
- version: "0.7.4-beta.1",
1393
+ version: "0.8.0-beta.3",
1355
1394
  description: "TinyCloud delegated operations MCP server",
1356
1395
  keywords: [
1357
1396
  "tinycloud",
@@ -1422,9 +1461,9 @@ var package_default = {
1422
1461
  dependencies: {
1423
1462
  "@modelcontextprotocol/node": "2.0.0-beta.5",
1424
1463
  "@modelcontextprotocol/server": "2.0.0-beta.5",
1425
- "@tinycloud/node-sdk": "3.6.0-beta.2",
1464
+ "@tinycloud/node-sdk": "3.6.0-beta.3",
1426
1465
  "@tinycloud/node-sdk-wasm": "1.7.8",
1427
- "@tinycloud/operations": "0.7.4-beta.1",
1466
+ "@tinycloud/operations": "0.7.4-beta.3",
1428
1467
  jose: "6.2.4"
1429
1468
  },
1430
1469
  devDependencies: {
@@ -1450,6 +1489,29 @@ var MCP_SERVER_NAME = "tinycloud-mcp";
1450
1489
  var REQUIRED_SCOPE = "tinycloud:mcp";
1451
1490
  function createHostedMcpApp(config) {
1452
1491
  const resourceUrl = canonicalResourceUrl(config.publicUrl);
1492
+ const pluginUrl = new URL("./chatgpt/mcp", resourceUrl);
1493
+ const hosted = createHostedSurface(config, resourceUrl, "hosted");
1494
+ const plugin = createHostedSurface({
1495
+ ...config,
1496
+ stateDir: join3(config.stateDir, "chatgpt"),
1497
+ approvalPath: new URL("./connect", pluginUrl).pathname
1498
+ }, pluginUrl, "public");
1499
+ const pluginPaths = {
1500
+ [pluginUrl.pathname]: true,
1501
+ [new URL("./connect", pluginUrl).pathname]: true,
1502
+ [new URL("./connect/callback", pluginUrl).pathname]: true,
1503
+ [new URL(getOAuthProtectedResourceMetadataUrl(pluginUrl)).pathname]: true
1504
+ };
1505
+ return {
1506
+ fetch: (request) => (pluginPaths[new URL(request.url).pathname] === true ? plugin : hosted).fetch(request),
1507
+ close: async () => {
1508
+ await Promise.all([hosted.close(), plugin.close()]);
1509
+ }
1510
+ };
1511
+ }
1512
+ function createHostedSurface(config, resourceUrl, surface) {
1513
+ const connectPath = config.approvalPath ?? "/connect";
1514
+ const callbackPath = `${connectPath}/callback`;
1453
1515
  const issuer = String(config.oauthMetadata.issuer);
1454
1516
  const jwksUri = String(config.oauthMetadata.jwks_uri);
1455
1517
  const verifier = createOpenKeyTokenVerifier({
@@ -1474,11 +1536,11 @@ function createHostedMcpApp(config) {
1474
1536
  { name: MCP_SERVER_NAME, version: MCP_VERSION },
1475
1537
  {
1476
1538
  jsonSchemaValidator: validator,
1477
- 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."
1539
+ 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."
1478
1540
  }
1479
1541
  );
1480
1542
  registerTinyCloudTools(server, {
1481
- surface: "hosted",
1543
+ surface,
1482
1544
  profile: "agent",
1483
1545
  explicitProfile: true,
1484
1546
  allowOwnerProfile: false,
@@ -1486,7 +1548,7 @@ function createHostedMcpApp(config) {
1486
1548
  beforeOperation: () => store.validateTenant(subject, ownerDids),
1487
1549
  transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
1488
1550
  }, validator);
1489
- registerConnectTool(server, validator, store, identity);
1551
+ registerConnectTool(server, validator, store, identity, surface);
1490
1552
  return server;
1491
1553
  }, {
1492
1554
  legacy: "stateless",
@@ -1497,7 +1559,7 @@ function createHostedMcpApp(config) {
1497
1559
  resourceServerUrl: resourceUrl,
1498
1560
  serviceDocumentationUrl: new URL("https://docs.tinycloud.xyz/cli/mcp"),
1499
1561
  scopesSupported: [REQUIRED_SCOPE],
1500
- resourceName: "TinyCloud hosted MCP"
1562
+ resourceName: surface === "public" ? "TinyCloud personal records" : "TinyCloud hosted MCP"
1501
1563
  };
1502
1564
  const allowedHosts = [resourceUrl.hostname];
1503
1565
  return {
@@ -1510,16 +1572,32 @@ function createHostedMcpApp(config) {
1510
1572
  }
1511
1573
  const hostFailure = hostHeaderValidationResponse(request, allowedHosts);
1512
1574
  if (hostFailure !== void 0) return hostFailure;
1513
- if (url.pathname === "/connect") {
1575
+ if (url.pathname === connectPath) {
1514
1576
  if (request.method !== "GET") return methodNotAllowed("GET");
1515
1577
  try {
1516
1578
  const destination = await store.approvalRedirect(requiredState(url));
1517
1579
  return Response.redirect(destination, 302);
1518
1580
  } catch {
1581
+ if (request.headers.get("accept")?.includes("text/html")) {
1582
+ return new Response(
1583
+ '<!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>',
1584
+ {
1585
+ status: 400,
1586
+ headers: {
1587
+ "content-type": "text/html; charset=utf-8",
1588
+ "cache-control": "no-store",
1589
+ "content-security-policy": "default-src 'none'; base-uri 'none'; frame-ancestors 'none'",
1590
+ "referrer-policy": "no-referrer",
1591
+ "x-content-type-options": "nosniff",
1592
+ vary: "Accept"
1593
+ }
1594
+ }
1595
+ );
1596
+ }
1519
1597
  return json({ error: "The approval link is invalid or expired." }, 400);
1520
1598
  }
1521
1599
  }
1522
- if (url.pathname === "/connect/callback") {
1600
+ if (url.pathname === callbackPath) {
1523
1601
  if (request.method === "OPTIONS") return callbackCors(new Response(null, { status: 204 }), config.openkeyHost);
1524
1602
  if (request.method !== "POST") return methodNotAllowed("POST");
1525
1603
  const originFailure2 = validateCallbackOrigin(request, config.openkeyHost);
@@ -1559,17 +1637,17 @@ function createHostedMcpApp(config) {
1559
1637
  close: () => handler.close()
1560
1638
  };
1561
1639
  }
1562
- function registerConnectTool(server, validator, store, identity) {
1640
+ function registerConnectTool(server, validator, store, identity, surface) {
1563
1641
  const inputSchema = fromJsonSchema2({ type: "object", properties: {}, additionalProperties: false }, validator);
1564
1642
  const outputSchema = fromJsonSchema2({
1565
1643
  type: "object",
1566
1644
  properties: {
1567
1645
  connected: { type: "boolean" },
1568
- sessionDid: { type: "string" },
1646
+ ...surface === "hosted" ? { sessionDid: { type: "string" } } : {},
1569
1647
  approvalUrl: { type: "string", format: "uri" },
1570
1648
  expiresAt: { type: "string", format: "date-time" }
1571
1649
  },
1572
- required: ["connected", "sessionDid"],
1650
+ required: surface === "hosted" ? ["connected", "sessionDid"] : ["connected"],
1573
1651
  additionalProperties: false
1574
1652
  }, validator);
1575
1653
  server.registerTool(
@@ -1583,7 +1661,7 @@ function registerConnectTool(server, validator, store, identity) {
1583
1661
  readOnlyHint: false,
1584
1662
  idempotentHint: true,
1585
1663
  destructiveHint: false,
1586
- openWorldHint: true
1664
+ openWorldHint: false
1587
1665
  }
1588
1666
  },
1589
1667
  async () => {
@@ -1592,12 +1670,17 @@ function registerConnectTool(server, validator, store, identity) {
1592
1670
  try {
1593
1671
  const status = await store.connectStatus(identity.subject, identity.ownerDids);
1594
1672
  code = status.connected ? "ok" : "authority_required";
1673
+ const visibleStatus = surface === "hosted" ? status : {
1674
+ connected: status.connected,
1675
+ ...status.approvalUrl === void 0 ? {} : { approvalUrl: status.approvalUrl },
1676
+ ...status.expiresAt === void 0 ? {} : { expiresAt: status.expiresAt }
1677
+ };
1595
1678
  return {
1596
1679
  content: [{
1597
1680
  type: "text",
1598
1681
  text: status.connected ? "TinyCloud is connected." : `TinyCloud approval is required. Open ${status.approvalUrl}, then call this tool again. This link expires at ${status.expiresAt}.`
1599
1682
  }],
1600
- structuredContent: status
1683
+ structuredContent: visibleStatus
1601
1684
  };
1602
1685
  } finally {
1603
1686
  logToolCall("tinycloud_connect", code, startedAt, 0);