@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.d.cts CHANGED
@@ -4,6 +4,8 @@ interface RemoteStoreConfig {
4
4
  readonly stateDir: string;
5
5
  readonly stateSecret: string;
6
6
  readonly publicUrl: URL;
7
+ /** Separate public-plugin approvals from the existing root callback. */
8
+ readonly approvalPath?: string;
7
9
  readonly nodeHost: string;
8
10
  readonly openkeyHost: string;
9
11
  readonly approvalTtlSeconds: number;
package/dist/http.d.ts CHANGED
@@ -4,6 +4,8 @@ interface RemoteStoreConfig {
4
4
  readonly stateDir: string;
5
5
  readonly stateSecret: string;
6
6
  readonly publicUrl: URL;
7
+ /** Separate public-plugin approvals from the existing root callback. */
8
+ readonly approvalPath?: string;
7
9
  readonly nodeHost: string;
8
10
  readonly openkeyHost: string;
9
11
  readonly approvalTtlSeconds: number;
package/dist/http.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // src/http.ts
2
+ import { join as join3 } from "path";
2
3
  import {
3
4
  McpServer as McpServer2,
4
5
  createMcpHandler,
@@ -352,7 +353,7 @@ var RemoteTenantStore = class {
352
353
  async approvalRedirect(state) {
353
354
  const pending = await this.#readPending(state, false);
354
355
  const tenant = await this.#readTenant(pending.tenantId);
355
- const callback = new URL("/connect/callback", this.#config.publicUrl);
356
+ const callback = new URL(`${this.#config.approvalPath ?? "/connect"}/callback`, this.#config.publicUrl);
356
357
  callback.searchParams.set("state", state);
357
358
  const params = new URLSearchParams({
358
359
  did: tenant.sessionDid,
@@ -771,7 +772,7 @@ var RemoteTenantStore = class {
771
772
  await rename(`${path}.processing`, path);
772
773
  }
773
774
  #connectUrl(pending) {
774
- const url = new URL("/connect", this.#config.publicUrl);
775
+ const url = new URL(this.#config.approvalPath ?? "/connect", this.#config.publicUrl);
775
776
  url.searchParams.set("state", `${pending.tenantId}.${pending.nonce}.${this.#signState(pending.tenantId, pending.nonce)}`);
776
777
  return url.toString();
777
778
  }
@@ -936,7 +937,7 @@ import { serveStdio } from "@modelcontextprotocol/server/stdio";
936
937
  import { createRequire } from "module";
937
938
  import { join as join2 } from "path";
938
939
  import { fromJsonSchema } from "@modelcontextprotocol/server";
939
- import { addFormats, Ajv, AjvJsonSchemaValidator } from "@modelcontextprotocol/server/validators/ajv";
940
+ import { AjvJsonSchemaValidator } from "@modelcontextprotocol/server/validators/ajv";
940
941
  import { invokeOperation as invokeOperation2 } from "@tinycloud/operations";
941
942
 
942
943
  // src/results.ts
@@ -950,7 +951,7 @@ function toMcpToolResult(result, bindings = [], options = {}) {
950
951
  let serialized;
951
952
  if (options.surface === "hosted") {
952
953
  serialized = JSON.stringify(canonical);
953
- if (options.readOnly === true && serialized.length > 1e5) {
954
+ if ((options.readOnly === true || options.operationId === "tinycloud.apps.context" || options.operationId === "tinycloud.apps.query") && serialized.length > 1e5) {
954
955
  const sql = options.operationId?.startsWith("tinycloud.sql.") || options.operationId === "tinycloud.apps.query";
955
956
  canonical = {
956
957
  status: "error",
@@ -1004,6 +1005,44 @@ function resultSummary(status) {
1004
1005
  }
1005
1006
  }
1006
1007
 
1008
+ // src/schemas.ts
1009
+ var DRAFT_7 = "http://json-schema.org/draft-07/schema#";
1010
+ var DRAFT_2020_12 = "https://json-schema.org/draft/2020-12/schema";
1011
+ var schemaMaps = /* @__PURE__ */ new Set(["properties", "patternProperties", "definitions", "$defs"]);
1012
+ var schemaArrays = /* @__PURE__ */ new Set(["allOf", "anyOf", "oneOf"]);
1013
+ var schemaValues = /* @__PURE__ */ new Set([
1014
+ "additionalProperties",
1015
+ "items",
1016
+ "contains",
1017
+ "propertyNames",
1018
+ "not",
1019
+ "if",
1020
+ "then",
1021
+ "else"
1022
+ ]);
1023
+ function toMcpJsonSchema(schema) {
1024
+ if (typeof schema === "boolean") return schema;
1025
+ if (Array.isArray(schema.items) || "additionalItems" in schema || "dependencies" in schema || "$ref" in schema && Object.keys(schema).some((key) => key !== "$ref")) {
1026
+ throw new Error("The operations schema needs a semantic migration to JSON Schema 2020-12.");
1027
+ }
1028
+ const projected = {};
1029
+ for (const [key, value] of Object.entries(schema)) {
1030
+ if (key === "$schema") {
1031
+ if (value !== DRAFT_7) throw new Error(`Unsupported operations schema dialect: ${String(value)}.`);
1032
+ projected[key] = DRAFT_2020_12;
1033
+ } else if (schemaMaps.has(key)) {
1034
+ projected[key] = Object.fromEntries(Object.entries(value).map(([name, child]) => [name, toMcpJsonSchema(child)]));
1035
+ } else if (schemaArrays.has(key)) {
1036
+ projected[key] = value.map(toMcpJsonSchema);
1037
+ } else if (schemaValues.has(key)) {
1038
+ projected[key] = toMcpJsonSchema(value);
1039
+ } else {
1040
+ projected[key] = value;
1041
+ }
1042
+ }
1043
+ return projected;
1044
+ }
1045
+
1007
1046
  // src/tools.ts
1008
1047
  function invocationTargetForMcp(selection) {
1009
1048
  return {
@@ -1021,9 +1060,9 @@ var generatedCatalog = packageRequire(
1021
1060
  var stableErrorCodes = Object.fromEntries(
1022
1061
  generatedCatalog.stableErrors.map((code) => [code, true])
1023
1062
  );
1024
- var ALL_SURFACES = ["local", "hosted"];
1063
+ var PRIVATE_SURFACES = ["local", "hosted"];
1025
1064
  function bindingSurfaces(binding) {
1026
- return binding.surfaces ?? ALL_SURFACES;
1065
+ return binding.surfaces ?? PRIVATE_SURFACES;
1027
1066
  }
1028
1067
  var TOOL_BINDINGS = [
1029
1068
  {
@@ -1207,11 +1246,11 @@ var TOOL_BINDINGS = [
1207
1246
  name: "tinycloud_apps_context",
1208
1247
  operationId: "tinycloud.apps.context",
1209
1248
  operationVersion: 1,
1210
- readOnlyHint: true,
1249
+ readOnlyHint: false,
1211
1250
  idempotentHint: true,
1212
- destructiveHint: false,
1213
- openWorldHint: true,
1214
- surfaces: ["hosted"]
1251
+ destructiveHint: true,
1252
+ openWorldHint: false,
1253
+ surfaces: ["hosted", "public"]
1215
1254
  },
1216
1255
  {
1217
1256
  name: "tinycloud_apps_read",
@@ -1220,8 +1259,8 @@ var TOOL_BINDINGS = [
1220
1259
  readOnlyHint: true,
1221
1260
  idempotentHint: true,
1222
1261
  destructiveHint: false,
1223
- openWorldHint: true,
1224
- surfaces: ["hosted"]
1262
+ openWorldHint: false,
1263
+ surfaces: ["hosted", "public"]
1225
1264
  },
1226
1265
  {
1227
1266
  name: "tinycloud_apps_write",
@@ -1230,34 +1269,34 @@ var TOOL_BINDINGS = [
1230
1269
  readOnlyHint: false,
1231
1270
  idempotentHint: false,
1232
1271
  destructiveHint: true,
1233
- openWorldHint: true,
1234
- surfaces: ["hosted"]
1272
+ openWorldHint: false,
1273
+ surfaces: ["hosted", "public"]
1235
1274
  },
1236
1275
  {
1237
1276
  name: "tinycloud_apps_query",
1238
1277
  operationId: "tinycloud.apps.query",
1239
1278
  operationVersion: 1,
1240
- readOnlyHint: true,
1279
+ readOnlyHint: false,
1241
1280
  idempotentHint: true,
1242
- destructiveHint: false,
1243
- openWorldHint: true,
1244
- surfaces: ["hosted"]
1281
+ destructiveHint: true,
1282
+ openWorldHint: false,
1283
+ surfaces: ["hosted", "public"]
1245
1284
  }
1246
1285
  ];
1286
+ var definitions = new Map(generatedCatalog.operations.map((operation) => [
1287
+ `${operation.id}@${operation.version}`,
1288
+ {
1289
+ ...operation,
1290
+ input: toMcpJsonSchema(operation.input),
1291
+ result: toMcpJsonSchema(operation.result)
1292
+ }
1293
+ ]));
1247
1294
  function createJsonSchemaValidator() {
1248
- const ajv = new Ajv({ strict: false });
1249
- addFormats(ajv);
1250
- return new AjvJsonSchemaValidator(ajv);
1295
+ return new AjvJsonSchemaValidator();
1251
1296
  }
1252
1297
  function registerTinyCloudTools(server, selection, validator) {
1253
- const definitions = new Map(
1254
- generatedCatalog.operations.map((operation) => [
1255
- `${operation.id}@${operation.version}`,
1256
- operation
1257
- ])
1258
- );
1259
- for (const binding of TOOL_BINDINGS) {
1260
- if (!bindingSurfaces(binding).includes(selection.surface)) continue;
1298
+ const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface));
1299
+ for (const binding of bindings) {
1261
1300
  const operation = definitions.get(`${binding.operationId}@${binding.operationVersion}`);
1262
1301
  if (operation === void 0) {
1263
1302
  throw new Error(`Generated operations catalog is missing ${binding.operationId}@${binding.operationVersion}.`);
@@ -1303,15 +1342,15 @@ function registerTinyCloudTools(server, selection, validator) {
1303
1342
  operationVersion: binding.operationVersion,
1304
1343
  input
1305
1344
  });
1306
- const response = toMcpToolResult(projected, TOOL_BINDINGS, {
1307
- surface: selection.surface,
1345
+ const response = toMcpToolResult(projected, bindings, {
1346
+ surface: selection.surface === "local" ? "local" : "hosted",
1308
1347
  readOnly: binding.readOnlyHint,
1309
1348
  operationId: binding.operationId
1310
1349
  });
1311
1350
  code = outcomeCode(response.structuredContent);
1312
1351
  return response;
1313
1352
  } finally {
1314
- if (selection.surface === "hosted") {
1353
+ if (selection.surface !== "local") {
1315
1354
  logToolCall(binding.name, code, startedAt, metrics.replayedGrants);
1316
1355
  }
1317
1356
  }
@@ -1334,7 +1373,7 @@ function outcomeCode(result) {
1334
1373
  // package.json
1335
1374
  var package_default = {
1336
1375
  name: "@tinycloud/mcp",
1337
- version: "0.7.4-beta.1",
1376
+ version: "0.8.0-beta.3",
1338
1377
  description: "TinyCloud delegated operations MCP server",
1339
1378
  keywords: [
1340
1379
  "tinycloud",
@@ -1405,9 +1444,9 @@ var package_default = {
1405
1444
  dependencies: {
1406
1445
  "@modelcontextprotocol/node": "2.0.0-beta.5",
1407
1446
  "@modelcontextprotocol/server": "2.0.0-beta.5",
1408
- "@tinycloud/node-sdk": "3.6.0-beta.2",
1447
+ "@tinycloud/node-sdk": "3.6.0-beta.3",
1409
1448
  "@tinycloud/node-sdk-wasm": "1.7.8",
1410
- "@tinycloud/operations": "0.7.4-beta.1",
1449
+ "@tinycloud/operations": "0.7.4-beta.3",
1411
1450
  jose: "6.2.4"
1412
1451
  },
1413
1452
  devDependencies: {
@@ -1433,6 +1472,29 @@ var MCP_SERVER_NAME = "tinycloud-mcp";
1433
1472
  var REQUIRED_SCOPE = "tinycloud:mcp";
1434
1473
  function createHostedMcpApp(config) {
1435
1474
  const resourceUrl = canonicalResourceUrl(config.publicUrl);
1475
+ const pluginUrl = new URL("./chatgpt/mcp", resourceUrl);
1476
+ const hosted = createHostedSurface(config, resourceUrl, "hosted");
1477
+ const plugin = createHostedSurface({
1478
+ ...config,
1479
+ stateDir: join3(config.stateDir, "chatgpt"),
1480
+ approvalPath: new URL("./connect", pluginUrl).pathname
1481
+ }, pluginUrl, "public");
1482
+ const pluginPaths = {
1483
+ [pluginUrl.pathname]: true,
1484
+ [new URL("./connect", pluginUrl).pathname]: true,
1485
+ [new URL("./connect/callback", pluginUrl).pathname]: true,
1486
+ [new URL(getOAuthProtectedResourceMetadataUrl(pluginUrl)).pathname]: true
1487
+ };
1488
+ return {
1489
+ fetch: (request) => (pluginPaths[new URL(request.url).pathname] === true ? plugin : hosted).fetch(request),
1490
+ close: async () => {
1491
+ await Promise.all([hosted.close(), plugin.close()]);
1492
+ }
1493
+ };
1494
+ }
1495
+ function createHostedSurface(config, resourceUrl, surface) {
1496
+ const connectPath = config.approvalPath ?? "/connect";
1497
+ const callbackPath = `${connectPath}/callback`;
1436
1498
  const issuer = String(config.oauthMetadata.issuer);
1437
1499
  const jwksUri = String(config.oauthMetadata.jwks_uri);
1438
1500
  const verifier = createOpenKeyTokenVerifier({
@@ -1457,11 +1519,11 @@ function createHostedMcpApp(config) {
1457
1519
  { name: MCP_SERVER_NAME, version: MCP_VERSION },
1458
1520
  {
1459
1521
  jsonSchemaValidator: validator,
1460
- 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."
1522
+ 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."
1461
1523
  }
1462
1524
  );
1463
1525
  registerTinyCloudTools(server, {
1464
- surface: "hosted",
1526
+ surface,
1465
1527
  profile: "agent",
1466
1528
  explicitProfile: true,
1467
1529
  allowOwnerProfile: false,
@@ -1469,7 +1531,7 @@ function createHostedMcpApp(config) {
1469
1531
  beforeOperation: () => store.validateTenant(subject, ownerDids),
1470
1532
  transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
1471
1533
  }, validator);
1472
- registerConnectTool(server, validator, store, identity);
1534
+ registerConnectTool(server, validator, store, identity, surface);
1473
1535
  return server;
1474
1536
  }, {
1475
1537
  legacy: "stateless",
@@ -1480,7 +1542,7 @@ function createHostedMcpApp(config) {
1480
1542
  resourceServerUrl: resourceUrl,
1481
1543
  serviceDocumentationUrl: new URL("https://docs.tinycloud.xyz/cli/mcp"),
1482
1544
  scopesSupported: [REQUIRED_SCOPE],
1483
- resourceName: "TinyCloud hosted MCP"
1545
+ resourceName: surface === "public" ? "TinyCloud personal records" : "TinyCloud hosted MCP"
1484
1546
  };
1485
1547
  const allowedHosts = [resourceUrl.hostname];
1486
1548
  return {
@@ -1493,16 +1555,32 @@ function createHostedMcpApp(config) {
1493
1555
  }
1494
1556
  const hostFailure = hostHeaderValidationResponse(request, allowedHosts);
1495
1557
  if (hostFailure !== void 0) return hostFailure;
1496
- if (url.pathname === "/connect") {
1558
+ if (url.pathname === connectPath) {
1497
1559
  if (request.method !== "GET") return methodNotAllowed("GET");
1498
1560
  try {
1499
1561
  const destination = await store.approvalRedirect(requiredState(url));
1500
1562
  return Response.redirect(destination, 302);
1501
1563
  } catch {
1564
+ if (request.headers.get("accept")?.includes("text/html")) {
1565
+ return new Response(
1566
+ '<!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>',
1567
+ {
1568
+ status: 400,
1569
+ headers: {
1570
+ "content-type": "text/html; charset=utf-8",
1571
+ "cache-control": "no-store",
1572
+ "content-security-policy": "default-src 'none'; base-uri 'none'; frame-ancestors 'none'",
1573
+ "referrer-policy": "no-referrer",
1574
+ "x-content-type-options": "nosniff",
1575
+ vary: "Accept"
1576
+ }
1577
+ }
1578
+ );
1579
+ }
1502
1580
  return json({ error: "The approval link is invalid or expired." }, 400);
1503
1581
  }
1504
1582
  }
1505
- if (url.pathname === "/connect/callback") {
1583
+ if (url.pathname === callbackPath) {
1506
1584
  if (request.method === "OPTIONS") return callbackCors(new Response(null, { status: 204 }), config.openkeyHost);
1507
1585
  if (request.method !== "POST") return methodNotAllowed("POST");
1508
1586
  const originFailure2 = validateCallbackOrigin(request, config.openkeyHost);
@@ -1542,17 +1620,17 @@ function createHostedMcpApp(config) {
1542
1620
  close: () => handler.close()
1543
1621
  };
1544
1622
  }
1545
- function registerConnectTool(server, validator, store, identity) {
1623
+ function registerConnectTool(server, validator, store, identity, surface) {
1546
1624
  const inputSchema = fromJsonSchema2({ type: "object", properties: {}, additionalProperties: false }, validator);
1547
1625
  const outputSchema = fromJsonSchema2({
1548
1626
  type: "object",
1549
1627
  properties: {
1550
1628
  connected: { type: "boolean" },
1551
- sessionDid: { type: "string" },
1629
+ ...surface === "hosted" ? { sessionDid: { type: "string" } } : {},
1552
1630
  approvalUrl: { type: "string", format: "uri" },
1553
1631
  expiresAt: { type: "string", format: "date-time" }
1554
1632
  },
1555
- required: ["connected", "sessionDid"],
1633
+ required: surface === "hosted" ? ["connected", "sessionDid"] : ["connected"],
1556
1634
  additionalProperties: false
1557
1635
  }, validator);
1558
1636
  server.registerTool(
@@ -1566,7 +1644,7 @@ function registerConnectTool(server, validator, store, identity) {
1566
1644
  readOnlyHint: false,
1567
1645
  idempotentHint: true,
1568
1646
  destructiveHint: false,
1569
- openWorldHint: true
1647
+ openWorldHint: false
1570
1648
  }
1571
1649
  },
1572
1650
  async () => {
@@ -1575,12 +1653,17 @@ function registerConnectTool(server, validator, store, identity) {
1575
1653
  try {
1576
1654
  const status = await store.connectStatus(identity.subject, identity.ownerDids);
1577
1655
  code = status.connected ? "ok" : "authority_required";
1656
+ const visibleStatus = surface === "hosted" ? status : {
1657
+ connected: status.connected,
1658
+ ...status.approvalUrl === void 0 ? {} : { approvalUrl: status.approvalUrl },
1659
+ ...status.expiresAt === void 0 ? {} : { expiresAt: status.expiresAt }
1660
+ };
1578
1661
  return {
1579
1662
  content: [{
1580
1663
  type: "text",
1581
1664
  text: status.connected ? "TinyCloud is connected." : `TinyCloud approval is required. Open ${status.approvalUrl}, then call this tool again. This link expires at ${status.expiresAt}.`
1582
1665
  }],
1583
- structuredContent: status
1666
+ structuredContent: visibleStatus
1584
1667
  };
1585
1668
  } finally {
1586
1669
  logToolCall("tinycloud_connect", code, startedAt, 0);