@tinycloud/mcp 0.7.4-beta.1 → 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/index.cjs CHANGED
@@ -60,7 +60,7 @@ function toMcpToolResult(result, bindings = [], options = {}) {
60
60
  let serialized;
61
61
  if (options.surface === "hosted") {
62
62
  serialized = JSON.stringify(canonical);
63
- if (options.readOnly === true && serialized.length > 1e5) {
63
+ if ((options.readOnly === true || options.operationId === "tinycloud.apps.context" || options.operationId === "tinycloud.apps.query") && serialized.length > 1e5) {
64
64
  const sql = options.operationId?.startsWith("tinycloud.sql.") || options.operationId === "tinycloud.apps.query";
65
65
  canonical = {
66
66
  status: "error",
@@ -128,6 +128,44 @@ function logToolCall(tool, code, startedAt, replayedGrants) {
128
128
  `);
129
129
  }
130
130
 
131
+ // src/schemas.ts
132
+ var DRAFT_7 = "http://json-schema.org/draft-07/schema#";
133
+ var DRAFT_2020_12 = "https://json-schema.org/draft/2020-12/schema";
134
+ var schemaMaps = /* @__PURE__ */ new Set(["properties", "patternProperties", "definitions", "$defs"]);
135
+ var schemaArrays = /* @__PURE__ */ new Set(["allOf", "anyOf", "oneOf"]);
136
+ var schemaValues = /* @__PURE__ */ new Set([
137
+ "additionalProperties",
138
+ "items",
139
+ "contains",
140
+ "propertyNames",
141
+ "not",
142
+ "if",
143
+ "then",
144
+ "else"
145
+ ]);
146
+ function toMcpJsonSchema(schema) {
147
+ if (typeof schema === "boolean") return schema;
148
+ if (Array.isArray(schema.items) || "additionalItems" in schema || "dependencies" in schema || "$ref" in schema && Object.keys(schema).some((key) => key !== "$ref")) {
149
+ throw new Error("The operations schema needs a semantic migration to JSON Schema 2020-12.");
150
+ }
151
+ const projected = {};
152
+ for (const [key, value] of Object.entries(schema)) {
153
+ if (key === "$schema") {
154
+ if (value !== DRAFT_7) throw new Error(`Unsupported operations schema dialect: ${String(value)}.`);
155
+ projected[key] = DRAFT_2020_12;
156
+ } else if (schemaMaps.has(key)) {
157
+ projected[key] = Object.fromEntries(Object.entries(value).map(([name, child]) => [name, toMcpJsonSchema(child)]));
158
+ } else if (schemaArrays.has(key)) {
159
+ projected[key] = value.map(toMcpJsonSchema);
160
+ } else if (schemaValues.has(key)) {
161
+ projected[key] = toMcpJsonSchema(value);
162
+ } else {
163
+ projected[key] = value;
164
+ }
165
+ }
166
+ return projected;
167
+ }
168
+
131
169
  // src/tools.ts
132
170
  var import_meta = {};
133
171
  var TOOL_NAMES = [
@@ -171,9 +209,9 @@ var generatedCatalog = packageRequire(
171
209
  var stableErrorCodes = Object.fromEntries(
172
210
  generatedCatalog.stableErrors.map((code) => [code, true])
173
211
  );
174
- var ALL_SURFACES = ["local", "hosted"];
212
+ var PRIVATE_SURFACES = ["local", "hosted"];
175
213
  function bindingSurfaces(binding) {
176
- return binding.surfaces ?? ALL_SURFACES;
214
+ return binding.surfaces ?? PRIVATE_SURFACES;
177
215
  }
178
216
  var TOOL_BINDINGS = [
179
217
  {
@@ -357,11 +395,11 @@ var TOOL_BINDINGS = [
357
395
  name: "tinycloud_apps_context",
358
396
  operationId: "tinycloud.apps.context",
359
397
  operationVersion: 1,
360
- readOnlyHint: true,
398
+ readOnlyHint: false,
361
399
  idempotentHint: true,
362
- destructiveHint: false,
363
- openWorldHint: true,
364
- surfaces: ["hosted"]
400
+ destructiveHint: true,
401
+ openWorldHint: false,
402
+ surfaces: ["hosted", "public"]
365
403
  },
366
404
  {
367
405
  name: "tinycloud_apps_read",
@@ -370,8 +408,8 @@ var TOOL_BINDINGS = [
370
408
  readOnlyHint: true,
371
409
  idempotentHint: true,
372
410
  destructiveHint: false,
373
- openWorldHint: true,
374
- surfaces: ["hosted"]
411
+ openWorldHint: false,
412
+ surfaces: ["hosted", "public"]
375
413
  },
376
414
  {
377
415
  name: "tinycloud_apps_write",
@@ -380,34 +418,34 @@ var TOOL_BINDINGS = [
380
418
  readOnlyHint: false,
381
419
  idempotentHint: false,
382
420
  destructiveHint: true,
383
- openWorldHint: true,
384
- surfaces: ["hosted"]
421
+ openWorldHint: false,
422
+ surfaces: ["hosted", "public"]
385
423
  },
386
424
  {
387
425
  name: "tinycloud_apps_query",
388
426
  operationId: "tinycloud.apps.query",
389
427
  operationVersion: 1,
390
- readOnlyHint: true,
428
+ readOnlyHint: false,
391
429
  idempotentHint: true,
392
- destructiveHint: false,
393
- openWorldHint: true,
394
- surfaces: ["hosted"]
430
+ destructiveHint: true,
431
+ openWorldHint: false,
432
+ surfaces: ["hosted", "public"]
395
433
  }
396
434
  ];
435
+ var definitions = new Map(generatedCatalog.operations.map((operation) => [
436
+ `${operation.id}@${operation.version}`,
437
+ {
438
+ ...operation,
439
+ input: toMcpJsonSchema(operation.input),
440
+ result: toMcpJsonSchema(operation.result)
441
+ }
442
+ ]));
397
443
  function createJsonSchemaValidator() {
398
- const ajv = new import_ajv.Ajv({ strict: false });
399
- (0, import_ajv.addFormats)(ajv);
400
- return new import_ajv.AjvJsonSchemaValidator(ajv);
444
+ return new import_ajv.AjvJsonSchemaValidator();
401
445
  }
402
446
  function registerTinyCloudTools(server, selection, validator) {
403
- const definitions = new Map(
404
- generatedCatalog.operations.map((operation) => [
405
- `${operation.id}@${operation.version}`,
406
- operation
407
- ])
408
- );
409
- for (const binding of TOOL_BINDINGS) {
410
- if (!bindingSurfaces(binding).includes(selection.surface)) continue;
447
+ const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface));
448
+ for (const binding of bindings) {
411
449
  const operation = definitions.get(`${binding.operationId}@${binding.operationVersion}`);
412
450
  if (operation === void 0) {
413
451
  throw new Error(`Generated operations catalog is missing ${binding.operationId}@${binding.operationVersion}.`);
@@ -453,15 +491,15 @@ function registerTinyCloudTools(server, selection, validator) {
453
491
  operationVersion: binding.operationVersion,
454
492
  input
455
493
  });
456
- const response = toMcpToolResult(projected, TOOL_BINDINGS, {
457
- surface: selection.surface,
494
+ const response = toMcpToolResult(projected, bindings, {
495
+ surface: selection.surface === "local" ? "local" : "hosted",
458
496
  readOnly: binding.readOnlyHint,
459
497
  operationId: binding.operationId
460
498
  });
461
499
  code = outcomeCode(response.structuredContent);
462
500
  return response;
463
501
  } finally {
464
- if (selection.surface === "hosted") {
502
+ if (selection.surface !== "local") {
465
503
  logToolCall(binding.name, code, startedAt, metrics.replayedGrants);
466
504
  }
467
505
  }
@@ -484,7 +522,7 @@ function outcomeCode(result) {
484
522
  // package.json
485
523
  var package_default = {
486
524
  name: "@tinycloud/mcp",
487
- version: "0.7.4-beta.1",
525
+ version: "0.8.0-beta.2",
488
526
  description: "TinyCloud delegated operations MCP server",
489
527
  keywords: [
490
528
  "tinycloud",
@@ -557,7 +595,7 @@ var package_default = {
557
595
  "@modelcontextprotocol/server": "2.0.0-beta.5",
558
596
  "@tinycloud/node-sdk": "3.6.0-beta.2",
559
597
  "@tinycloud/node-sdk-wasm": "1.7.8",
560
- "@tinycloud/operations": "0.7.4-beta.1",
598
+ "@tinycloud/operations": "0.7.4-beta.2",
561
599
  jose: "6.2.4"
562
600
  },
563
601
  devDependencies: {
@@ -696,6 +734,7 @@ if (isDirectInvocation()) {
696
734
  }
697
735
 
698
736
  // src/http.ts
737
+ var import_node_path4 = require("path");
699
738
  var import_server5 = require("@modelcontextprotocol/server");
700
739
  var import_node_sdk3 = require("@tinycloud/node-sdk");
701
740
 
@@ -1018,7 +1057,7 @@ var RemoteTenantStore = class {
1018
1057
  async approvalRedirect(state) {
1019
1058
  const pending = await this.#readPending(state, false);
1020
1059
  const tenant = await this.#readTenant(pending.tenantId);
1021
- const callback = new URL("/connect/callback", this.#config.publicUrl);
1060
+ const callback = new URL(`${this.#config.approvalPath ?? "/connect"}/callback`, this.#config.publicUrl);
1022
1061
  callback.searchParams.set("state", state);
1023
1062
  const params = new URLSearchParams({
1024
1063
  did: tenant.sessionDid,
@@ -1437,7 +1476,7 @@ var RemoteTenantStore = class {
1437
1476
  await (0, import_promises.rename)(`${path}.processing`, path);
1438
1477
  }
1439
1478
  #connectUrl(pending) {
1440
- const url = new URL("/connect", this.#config.publicUrl);
1479
+ const url = new URL(this.#config.approvalPath ?? "/connect", this.#config.publicUrl);
1441
1480
  url.searchParams.set("state", `${pending.tenantId}.${pending.nonce}.${this.#signState(pending.tenantId, pending.nonce)}`);
1442
1481
  return url.toString();
1443
1482
  }
@@ -1598,6 +1637,29 @@ function samePendingRequest(pending, expected) {
1598
1637
  var REQUIRED_SCOPE = "tinycloud:mcp";
1599
1638
  function createHostedMcpApp(config) {
1600
1639
  const resourceUrl = canonicalResourceUrl(config.publicUrl);
1640
+ const pluginUrl = new URL("./chatgpt/mcp", resourceUrl);
1641
+ const hosted = createHostedSurface(config, resourceUrl, "hosted");
1642
+ const plugin = createHostedSurface({
1643
+ ...config,
1644
+ stateDir: (0, import_node_path4.join)(config.stateDir, "chatgpt"),
1645
+ approvalPath: new URL("./connect", pluginUrl).pathname
1646
+ }, pluginUrl, "public");
1647
+ const pluginPaths = {
1648
+ [pluginUrl.pathname]: true,
1649
+ [new URL("./connect", pluginUrl).pathname]: true,
1650
+ [new URL("./connect/callback", pluginUrl).pathname]: true,
1651
+ [new URL((0, import_server5.getOAuthProtectedResourceMetadataUrl)(pluginUrl)).pathname]: true
1652
+ };
1653
+ return {
1654
+ fetch: (request) => (pluginPaths[new URL(request.url).pathname] === true ? plugin : hosted).fetch(request),
1655
+ close: async () => {
1656
+ await Promise.all([hosted.close(), plugin.close()]);
1657
+ }
1658
+ };
1659
+ }
1660
+ function createHostedSurface(config, resourceUrl, surface) {
1661
+ const connectPath = config.approvalPath ?? "/connect";
1662
+ const callbackPath = `${connectPath}/callback`;
1601
1663
  const issuer = String(config.oauthMetadata.issuer);
1602
1664
  const jwksUri = String(config.oauthMetadata.jwks_uri);
1603
1665
  const verifier = createOpenKeyTokenVerifier({
@@ -1622,11 +1684,11 @@ function createHostedMcpApp(config) {
1622
1684
  { name: MCP_SERVER_NAME, version: MCP_VERSION },
1623
1685
  {
1624
1686
  jsonSchemaValidator: validator,
1625
- 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."
1687
+ 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."
1626
1688
  }
1627
1689
  );
1628
1690
  registerTinyCloudTools(server, {
1629
- surface: "hosted",
1691
+ surface,
1630
1692
  profile: "agent",
1631
1693
  explicitProfile: true,
1632
1694
  allowOwnerProfile: false,
@@ -1634,7 +1696,7 @@ function createHostedMcpApp(config) {
1634
1696
  beforeOperation: () => store.validateTenant(subject, ownerDids),
1635
1697
  transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
1636
1698
  }, validator);
1637
- registerConnectTool(server, validator, store, identity);
1699
+ registerConnectTool(server, validator, store, identity, surface);
1638
1700
  return server;
1639
1701
  }, {
1640
1702
  legacy: "stateless",
@@ -1645,7 +1707,7 @@ function createHostedMcpApp(config) {
1645
1707
  resourceServerUrl: resourceUrl,
1646
1708
  serviceDocumentationUrl: new URL("https://docs.tinycloud.xyz/cli/mcp"),
1647
1709
  scopesSupported: [REQUIRED_SCOPE],
1648
- resourceName: "TinyCloud hosted MCP"
1710
+ resourceName: surface === "public" ? "TinyCloud personal records" : "TinyCloud hosted MCP"
1649
1711
  };
1650
1712
  const allowedHosts = [resourceUrl.hostname];
1651
1713
  return {
@@ -1658,16 +1720,32 @@ function createHostedMcpApp(config) {
1658
1720
  }
1659
1721
  const hostFailure = (0, import_server5.hostHeaderValidationResponse)(request, allowedHosts);
1660
1722
  if (hostFailure !== void 0) return hostFailure;
1661
- if (url.pathname === "/connect") {
1723
+ if (url.pathname === connectPath) {
1662
1724
  if (request.method !== "GET") return methodNotAllowed("GET");
1663
1725
  try {
1664
1726
  const destination = await store.approvalRedirect(requiredState(url));
1665
1727
  return Response.redirect(destination, 302);
1666
1728
  } catch {
1729
+ if (request.headers.get("accept")?.includes("text/html")) {
1730
+ return new Response(
1731
+ '<!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>',
1732
+ {
1733
+ status: 400,
1734
+ headers: {
1735
+ "content-type": "text/html; charset=utf-8",
1736
+ "cache-control": "no-store",
1737
+ "content-security-policy": "default-src 'none'; base-uri 'none'; frame-ancestors 'none'",
1738
+ "referrer-policy": "no-referrer",
1739
+ "x-content-type-options": "nosniff",
1740
+ vary: "Accept"
1741
+ }
1742
+ }
1743
+ );
1744
+ }
1667
1745
  return json({ error: "The approval link is invalid or expired." }, 400);
1668
1746
  }
1669
1747
  }
1670
- if (url.pathname === "/connect/callback") {
1748
+ if (url.pathname === callbackPath) {
1671
1749
  if (request.method === "OPTIONS") return callbackCors(new Response(null, { status: 204 }), config.openkeyHost);
1672
1750
  if (request.method !== "POST") return methodNotAllowed("POST");
1673
1751
  const originFailure2 = validateCallbackOrigin(request, config.openkeyHost);
@@ -1707,17 +1785,17 @@ function createHostedMcpApp(config) {
1707
1785
  close: () => handler.close()
1708
1786
  };
1709
1787
  }
1710
- function registerConnectTool(server, validator, store, identity) {
1788
+ function registerConnectTool(server, validator, store, identity, surface) {
1711
1789
  const inputSchema = (0, import_server5.fromJsonSchema)({ type: "object", properties: {}, additionalProperties: false }, validator);
1712
1790
  const outputSchema = (0, import_server5.fromJsonSchema)({
1713
1791
  type: "object",
1714
1792
  properties: {
1715
1793
  connected: { type: "boolean" },
1716
- sessionDid: { type: "string" },
1794
+ ...surface === "hosted" ? { sessionDid: { type: "string" } } : {},
1717
1795
  approvalUrl: { type: "string", format: "uri" },
1718
1796
  expiresAt: { type: "string", format: "date-time" }
1719
1797
  },
1720
- required: ["connected", "sessionDid"],
1798
+ required: surface === "hosted" ? ["connected", "sessionDid"] : ["connected"],
1721
1799
  additionalProperties: false
1722
1800
  }, validator);
1723
1801
  server.registerTool(
@@ -1731,7 +1809,7 @@ function registerConnectTool(server, validator, store, identity) {
1731
1809
  readOnlyHint: false,
1732
1810
  idempotentHint: true,
1733
1811
  destructiveHint: false,
1734
- openWorldHint: true
1812
+ openWorldHint: false
1735
1813
  }
1736
1814
  },
1737
1815
  async () => {
@@ -1740,12 +1818,17 @@ function registerConnectTool(server, validator, store, identity) {
1740
1818
  try {
1741
1819
  const status = await store.connectStatus(identity.subject, identity.ownerDids);
1742
1820
  code = status.connected ? "ok" : "authority_required";
1821
+ const visibleStatus = surface === "hosted" ? status : {
1822
+ connected: status.connected,
1823
+ ...status.approvalUrl === void 0 ? {} : { approvalUrl: status.approvalUrl },
1824
+ ...status.expiresAt === void 0 ? {} : { expiresAt: status.expiresAt }
1825
+ };
1743
1826
  return {
1744
1827
  content: [{
1745
1828
  type: "text",
1746
1829
  text: status.connected ? "TinyCloud is connected." : `TinyCloud approval is required. Open ${status.approvalUrl}, then call this tool again. This link expires at ${status.expiresAt}.`
1747
1830
  }],
1748
- structuredContent: status
1831
+ structuredContent: visibleStatus
1749
1832
  };
1750
1833
  } finally {
1751
1834
  logToolCall("tinycloud_connect", code, startedAt, 0);