@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/index.js CHANGED
@@ -10,7 +10,7 @@ import { serveStdio } from "@modelcontextprotocol/server/stdio";
10
10
  import { createRequire } from "module";
11
11
  import { join } from "path";
12
12
  import { fromJsonSchema } from "@modelcontextprotocol/server";
13
- import { addFormats, Ajv, AjvJsonSchemaValidator } from "@modelcontextprotocol/server/validators/ajv";
13
+ import { AjvJsonSchemaValidator } from "@modelcontextprotocol/server/validators/ajv";
14
14
  import { invokeOperation } from "@tinycloud/operations";
15
15
 
16
16
  // src/results.ts
@@ -24,7 +24,7 @@ function toMcpToolResult(result, bindings = [], options = {}) {
24
24
  let serialized;
25
25
  if (options.surface === "hosted") {
26
26
  serialized = JSON.stringify(canonical);
27
- if (options.readOnly === true && serialized.length > 1e5) {
27
+ if ((options.readOnly === true || options.operationId === "tinycloud.apps.context" || options.operationId === "tinycloud.apps.query") && serialized.length > 1e5) {
28
28
  const sql = options.operationId?.startsWith("tinycloud.sql.") || options.operationId === "tinycloud.apps.query";
29
29
  canonical = {
30
30
  status: "error",
@@ -92,6 +92,44 @@ function logToolCall(tool, code, startedAt, replayedGrants) {
92
92
  `);
93
93
  }
94
94
 
95
+ // src/schemas.ts
96
+ var DRAFT_7 = "http://json-schema.org/draft-07/schema#";
97
+ var DRAFT_2020_12 = "https://json-schema.org/draft/2020-12/schema";
98
+ var schemaMaps = /* @__PURE__ */ new Set(["properties", "patternProperties", "definitions", "$defs"]);
99
+ var schemaArrays = /* @__PURE__ */ new Set(["allOf", "anyOf", "oneOf"]);
100
+ var schemaValues = /* @__PURE__ */ new Set([
101
+ "additionalProperties",
102
+ "items",
103
+ "contains",
104
+ "propertyNames",
105
+ "not",
106
+ "if",
107
+ "then",
108
+ "else"
109
+ ]);
110
+ function toMcpJsonSchema(schema) {
111
+ if (typeof schema === "boolean") return schema;
112
+ if (Array.isArray(schema.items) || "additionalItems" in schema || "dependencies" in schema || "$ref" in schema && Object.keys(schema).some((key) => key !== "$ref")) {
113
+ throw new Error("The operations schema needs a semantic migration to JSON Schema 2020-12.");
114
+ }
115
+ const projected = {};
116
+ for (const [key, value] of Object.entries(schema)) {
117
+ if (key === "$schema") {
118
+ if (value !== DRAFT_7) throw new Error(`Unsupported operations schema dialect: ${String(value)}.`);
119
+ projected[key] = DRAFT_2020_12;
120
+ } else if (schemaMaps.has(key)) {
121
+ projected[key] = Object.fromEntries(Object.entries(value).map(([name, child]) => [name, toMcpJsonSchema(child)]));
122
+ } else if (schemaArrays.has(key)) {
123
+ projected[key] = value.map(toMcpJsonSchema);
124
+ } else if (schemaValues.has(key)) {
125
+ projected[key] = toMcpJsonSchema(value);
126
+ } else {
127
+ projected[key] = value;
128
+ }
129
+ }
130
+ return projected;
131
+ }
132
+
95
133
  // src/tools.ts
96
134
  var TOOL_NAMES = [
97
135
  "tinycloud_status",
@@ -134,9 +172,9 @@ var generatedCatalog = packageRequire(
134
172
  var stableErrorCodes = Object.fromEntries(
135
173
  generatedCatalog.stableErrors.map((code) => [code, true])
136
174
  );
137
- var ALL_SURFACES = ["local", "hosted"];
175
+ var PRIVATE_SURFACES = ["local", "hosted"];
138
176
  function bindingSurfaces(binding) {
139
- return binding.surfaces ?? ALL_SURFACES;
177
+ return binding.surfaces ?? PRIVATE_SURFACES;
140
178
  }
141
179
  var TOOL_BINDINGS = [
142
180
  {
@@ -320,11 +358,11 @@ var TOOL_BINDINGS = [
320
358
  name: "tinycloud_apps_context",
321
359
  operationId: "tinycloud.apps.context",
322
360
  operationVersion: 1,
323
- readOnlyHint: true,
361
+ readOnlyHint: false,
324
362
  idempotentHint: true,
325
- destructiveHint: false,
326
- openWorldHint: true,
327
- surfaces: ["hosted"]
363
+ destructiveHint: true,
364
+ openWorldHint: false,
365
+ surfaces: ["hosted", "public"]
328
366
  },
329
367
  {
330
368
  name: "tinycloud_apps_read",
@@ -333,8 +371,8 @@ var TOOL_BINDINGS = [
333
371
  readOnlyHint: true,
334
372
  idempotentHint: true,
335
373
  destructiveHint: false,
336
- openWorldHint: true,
337
- surfaces: ["hosted"]
374
+ openWorldHint: false,
375
+ surfaces: ["hosted", "public"]
338
376
  },
339
377
  {
340
378
  name: "tinycloud_apps_write",
@@ -343,34 +381,34 @@ var TOOL_BINDINGS = [
343
381
  readOnlyHint: false,
344
382
  idempotentHint: false,
345
383
  destructiveHint: true,
346
- openWorldHint: true,
347
- surfaces: ["hosted"]
384
+ openWorldHint: false,
385
+ surfaces: ["hosted", "public"]
348
386
  },
349
387
  {
350
388
  name: "tinycloud_apps_query",
351
389
  operationId: "tinycloud.apps.query",
352
390
  operationVersion: 1,
353
- readOnlyHint: true,
391
+ readOnlyHint: false,
354
392
  idempotentHint: true,
355
- destructiveHint: false,
356
- openWorldHint: true,
357
- surfaces: ["hosted"]
393
+ destructiveHint: true,
394
+ openWorldHint: false,
395
+ surfaces: ["hosted", "public"]
358
396
  }
359
397
  ];
398
+ var definitions = new Map(generatedCatalog.operations.map((operation) => [
399
+ `${operation.id}@${operation.version}`,
400
+ {
401
+ ...operation,
402
+ input: toMcpJsonSchema(operation.input),
403
+ result: toMcpJsonSchema(operation.result)
404
+ }
405
+ ]));
360
406
  function createJsonSchemaValidator() {
361
- const ajv = new Ajv({ strict: false });
362
- addFormats(ajv);
363
- return new AjvJsonSchemaValidator(ajv);
407
+ return new AjvJsonSchemaValidator();
364
408
  }
365
409
  function registerTinyCloudTools(server, selection, validator) {
366
- const definitions = new Map(
367
- generatedCatalog.operations.map((operation) => [
368
- `${operation.id}@${operation.version}`,
369
- operation
370
- ])
371
- );
372
- for (const binding of TOOL_BINDINGS) {
373
- if (!bindingSurfaces(binding).includes(selection.surface)) continue;
410
+ const bindings = TOOL_BINDINGS.filter((binding) => bindingSurfaces(binding).includes(selection.surface));
411
+ for (const binding of bindings) {
374
412
  const operation = definitions.get(`${binding.operationId}@${binding.operationVersion}`);
375
413
  if (operation === void 0) {
376
414
  throw new Error(`Generated operations catalog is missing ${binding.operationId}@${binding.operationVersion}.`);
@@ -416,15 +454,15 @@ function registerTinyCloudTools(server, selection, validator) {
416
454
  operationVersion: binding.operationVersion,
417
455
  input
418
456
  });
419
- const response = toMcpToolResult(projected, TOOL_BINDINGS, {
420
- surface: selection.surface,
457
+ const response = toMcpToolResult(projected, bindings, {
458
+ surface: selection.surface === "local" ? "local" : "hosted",
421
459
  readOnly: binding.readOnlyHint,
422
460
  operationId: binding.operationId
423
461
  });
424
462
  code = outcomeCode(response.structuredContent);
425
463
  return response;
426
464
  } finally {
427
- if (selection.surface === "hosted") {
465
+ if (selection.surface !== "local") {
428
466
  logToolCall(binding.name, code, startedAt, metrics.replayedGrants);
429
467
  }
430
468
  }
@@ -447,7 +485,7 @@ function outcomeCode(result) {
447
485
  // package.json
448
486
  var package_default = {
449
487
  name: "@tinycloud/mcp",
450
- version: "0.7.4-beta.0",
488
+ version: "0.8.0-beta.2",
451
489
  description: "TinyCloud delegated operations MCP server",
452
490
  keywords: [
453
491
  "tinycloud",
@@ -518,9 +556,9 @@ var package_default = {
518
556
  dependencies: {
519
557
  "@modelcontextprotocol/node": "2.0.0-beta.5",
520
558
  "@modelcontextprotocol/server": "2.0.0-beta.5",
521
- "@tinycloud/node-sdk": "3.6.0-beta.0",
559
+ "@tinycloud/node-sdk": "3.6.0-beta.2",
522
560
  "@tinycloud/node-sdk-wasm": "1.7.8",
523
- "@tinycloud/operations": "0.7.4-beta.0",
561
+ "@tinycloud/operations": "0.7.4-beta.2",
524
562
  jose: "6.2.4"
525
563
  },
526
564
  devDependencies: {
@@ -659,6 +697,7 @@ if (isDirectInvocation()) {
659
697
  }
660
698
 
661
699
  // src/http.ts
700
+ import { join as join3 } from "path";
662
701
  import {
663
702
  McpServer as McpServer2,
664
703
  createMcpHandler,
@@ -1001,7 +1040,7 @@ var RemoteTenantStore = class {
1001
1040
  async approvalRedirect(state) {
1002
1041
  const pending = await this.#readPending(state, false);
1003
1042
  const tenant = await this.#readTenant(pending.tenantId);
1004
- const callback = new URL("/connect/callback", this.#config.publicUrl);
1043
+ const callback = new URL(`${this.#config.approvalPath ?? "/connect"}/callback`, this.#config.publicUrl);
1005
1044
  callback.searchParams.set("state", state);
1006
1045
  const params = new URLSearchParams({
1007
1046
  did: tenant.sessionDid,
@@ -1420,7 +1459,7 @@ var RemoteTenantStore = class {
1420
1459
  await rename(`${path}.processing`, path);
1421
1460
  }
1422
1461
  #connectUrl(pending) {
1423
- const url = new URL("/connect", this.#config.publicUrl);
1462
+ const url = new URL(this.#config.approvalPath ?? "/connect", this.#config.publicUrl);
1424
1463
  url.searchParams.set("state", `${pending.tenantId}.${pending.nonce}.${this.#signState(pending.tenantId, pending.nonce)}`);
1425
1464
  return url.toString();
1426
1465
  }
@@ -1581,6 +1620,29 @@ function samePendingRequest(pending, expected) {
1581
1620
  var REQUIRED_SCOPE = "tinycloud:mcp";
1582
1621
  function createHostedMcpApp(config) {
1583
1622
  const resourceUrl = canonicalResourceUrl(config.publicUrl);
1623
+ const pluginUrl = new URL("./chatgpt/mcp", resourceUrl);
1624
+ const hosted = createHostedSurface(config, resourceUrl, "hosted");
1625
+ const plugin = createHostedSurface({
1626
+ ...config,
1627
+ stateDir: join3(config.stateDir, "chatgpt"),
1628
+ approvalPath: new URL("./connect", pluginUrl).pathname
1629
+ }, pluginUrl, "public");
1630
+ const pluginPaths = {
1631
+ [pluginUrl.pathname]: true,
1632
+ [new URL("./connect", pluginUrl).pathname]: true,
1633
+ [new URL("./connect/callback", pluginUrl).pathname]: true,
1634
+ [new URL(getOAuthProtectedResourceMetadataUrl(pluginUrl)).pathname]: true
1635
+ };
1636
+ return {
1637
+ fetch: (request) => (pluginPaths[new URL(request.url).pathname] === true ? plugin : hosted).fetch(request),
1638
+ close: async () => {
1639
+ await Promise.all([hosted.close(), plugin.close()]);
1640
+ }
1641
+ };
1642
+ }
1643
+ function createHostedSurface(config, resourceUrl, surface) {
1644
+ const connectPath = config.approvalPath ?? "/connect";
1645
+ const callbackPath = `${connectPath}/callback`;
1584
1646
  const issuer = String(config.oauthMetadata.issuer);
1585
1647
  const jwksUri = String(config.oauthMetadata.jwks_uri);
1586
1648
  const verifier = createOpenKeyTokenVerifier({
@@ -1605,11 +1667,11 @@ function createHostedMcpApp(config) {
1605
1667
  { name: MCP_SERVER_NAME, version: MCP_VERSION },
1606
1668
  {
1607
1669
  jsonSchemaValidator: validator,
1608
- 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."
1670
+ 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."
1609
1671
  }
1610
1672
  );
1611
1673
  registerTinyCloudTools(server, {
1612
- surface: "hosted",
1674
+ surface,
1613
1675
  profile: "agent",
1614
1676
  explicitProfile: true,
1615
1677
  allowOwnerProfile: false,
@@ -1617,7 +1679,7 @@ function createHostedMcpApp(config) {
1617
1679
  beforeOperation: () => store.validateTenant(subject, ownerDids),
1618
1680
  transformOperationResult: (result, invocation) => store.decorateAuthorityResult(subject, ownerDids, result, invocation)
1619
1681
  }, validator);
1620
- registerConnectTool(server, validator, store, identity);
1682
+ registerConnectTool(server, validator, store, identity, surface);
1621
1683
  return server;
1622
1684
  }, {
1623
1685
  legacy: "stateless",
@@ -1628,7 +1690,7 @@ function createHostedMcpApp(config) {
1628
1690
  resourceServerUrl: resourceUrl,
1629
1691
  serviceDocumentationUrl: new URL("https://docs.tinycloud.xyz/cli/mcp"),
1630
1692
  scopesSupported: [REQUIRED_SCOPE],
1631
- resourceName: "TinyCloud hosted MCP"
1693
+ resourceName: surface === "public" ? "TinyCloud personal records" : "TinyCloud hosted MCP"
1632
1694
  };
1633
1695
  const allowedHosts = [resourceUrl.hostname];
1634
1696
  return {
@@ -1641,16 +1703,32 @@ function createHostedMcpApp(config) {
1641
1703
  }
1642
1704
  const hostFailure = hostHeaderValidationResponse(request, allowedHosts);
1643
1705
  if (hostFailure !== void 0) return hostFailure;
1644
- if (url.pathname === "/connect") {
1706
+ if (url.pathname === connectPath) {
1645
1707
  if (request.method !== "GET") return methodNotAllowed("GET");
1646
1708
  try {
1647
1709
  const destination = await store.approvalRedirect(requiredState(url));
1648
1710
  return Response.redirect(destination, 302);
1649
1711
  } catch {
1712
+ if (request.headers.get("accept")?.includes("text/html")) {
1713
+ return new Response(
1714
+ '<!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>',
1715
+ {
1716
+ status: 400,
1717
+ headers: {
1718
+ "content-type": "text/html; charset=utf-8",
1719
+ "cache-control": "no-store",
1720
+ "content-security-policy": "default-src 'none'; base-uri 'none'; frame-ancestors 'none'",
1721
+ "referrer-policy": "no-referrer",
1722
+ "x-content-type-options": "nosniff",
1723
+ vary: "Accept"
1724
+ }
1725
+ }
1726
+ );
1727
+ }
1650
1728
  return json({ error: "The approval link is invalid or expired." }, 400);
1651
1729
  }
1652
1730
  }
1653
- if (url.pathname === "/connect/callback") {
1731
+ if (url.pathname === callbackPath) {
1654
1732
  if (request.method === "OPTIONS") return callbackCors(new Response(null, { status: 204 }), config.openkeyHost);
1655
1733
  if (request.method !== "POST") return methodNotAllowed("POST");
1656
1734
  const originFailure2 = validateCallbackOrigin(request, config.openkeyHost);
@@ -1690,17 +1768,17 @@ function createHostedMcpApp(config) {
1690
1768
  close: () => handler.close()
1691
1769
  };
1692
1770
  }
1693
- function registerConnectTool(server, validator, store, identity) {
1771
+ function registerConnectTool(server, validator, store, identity, surface) {
1694
1772
  const inputSchema = fromJsonSchema2({ type: "object", properties: {}, additionalProperties: false }, validator);
1695
1773
  const outputSchema = fromJsonSchema2({
1696
1774
  type: "object",
1697
1775
  properties: {
1698
1776
  connected: { type: "boolean" },
1699
- sessionDid: { type: "string" },
1777
+ ...surface === "hosted" ? { sessionDid: { type: "string" } } : {},
1700
1778
  approvalUrl: { type: "string", format: "uri" },
1701
1779
  expiresAt: { type: "string", format: "date-time" }
1702
1780
  },
1703
- required: ["connected", "sessionDid"],
1781
+ required: surface === "hosted" ? ["connected", "sessionDid"] : ["connected"],
1704
1782
  additionalProperties: false
1705
1783
  }, validator);
1706
1784
  server.registerTool(
@@ -1714,7 +1792,7 @@ function registerConnectTool(server, validator, store, identity) {
1714
1792
  readOnlyHint: false,
1715
1793
  idempotentHint: true,
1716
1794
  destructiveHint: false,
1717
- openWorldHint: true
1795
+ openWorldHint: false
1718
1796
  }
1719
1797
  },
1720
1798
  async () => {
@@ -1723,12 +1801,17 @@ function registerConnectTool(server, validator, store, identity) {
1723
1801
  try {
1724
1802
  const status = await store.connectStatus(identity.subject, identity.ownerDids);
1725
1803
  code = status.connected ? "ok" : "authority_required";
1804
+ const visibleStatus = surface === "hosted" ? status : {
1805
+ connected: status.connected,
1806
+ ...status.approvalUrl === void 0 ? {} : { approvalUrl: status.approvalUrl },
1807
+ ...status.expiresAt === void 0 ? {} : { expiresAt: status.expiresAt }
1808
+ };
1726
1809
  return {
1727
1810
  content: [{
1728
1811
  type: "text",
1729
1812
  text: status.connected ? "TinyCloud is connected." : `TinyCloud approval is required. Open ${status.approvalUrl}, then call this tool again. This link expires at ${status.expiresAt}.`
1730
1813
  }],
1731
- structuredContent: status
1814
+ structuredContent: visibleStatus
1732
1815
  };
1733
1816
  } finally {
1734
1817
  logToolCall("tinycloud_connect", code, startedAt, 0);