@tinycloud/mcp 0.5.5-beta.0 → 0.6.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.cjs CHANGED
@@ -103,6 +103,17 @@ function firstString(...values) {
103
103
  return values.find((value) => typeof value === "string" && value.length > 0);
104
104
  }
105
105
 
106
+ // src/logging.ts
107
+ function logToolCall(tool, code, startedAt, replayedGrants) {
108
+ process.stderr.write(`${JSON.stringify({
109
+ tool,
110
+ code,
111
+ durationMs: Math.max(0, Math.round(performance.now() - startedAt)),
112
+ replayedGrants
113
+ })}
114
+ `);
115
+ }
116
+
106
117
  // src/remote-store.ts
107
118
  var import_node_crypto = require("crypto");
108
119
  var import_promises = require("fs/promises");
@@ -110,6 +121,7 @@ var import_node_path = require("path");
110
121
  var import_node_sdk2 = require("@tinycloud/node-sdk");
111
122
  var import_node_sdk_wasm = require("@tinycloud/node-sdk-wasm");
112
123
  var import_operations = require("@tinycloud/operations");
124
+ var import_tc_apps = require("@tinycloud/operations/tc-apps");
113
125
  var PROFILE = "agent";
114
126
  var MAX_CALLBACK_BYTES = 256 * 1024;
115
127
  var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
@@ -162,6 +174,10 @@ var RemoteTenantStore = class {
162
174
  tenantStateRoot(subject) {
163
175
  return (0, import_node_path.join)(this.#config.stateDir, "tenants", this.#tenantId(subject));
164
176
  }
177
+ /** Prepare only this authenticated account's profile before its first operation. */
178
+ async initializeTenant(subject) {
179
+ return (await this.#ensureTenant(subject)).stateRoot;
180
+ }
165
181
  async connectStatus(subject, ownerDids = []) {
166
182
  const tenant = await this.#ensureTenant(subject);
167
183
  if (await this.#hasValidSession(tenant, ownerDids)) {
@@ -176,7 +192,23 @@ var RemoteTenantStore = class {
176
192
  };
177
193
  }
178
194
  async decorateAuthorityResult(subject, ownerDids, result) {
179
- if (!isRecord(result) || result.status !== "authority_required") return result;
195
+ if (!isRecord(result)) return result;
196
+ if (result.status === "error" && isRecord(result.error) && (result.error.code === "SESSION_NOT_FOUND" || result.error.code === "AUTH_REQUIRED")) {
197
+ await this.#ensureTenant(subject);
198
+ const pending2 = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
199
+ return {
200
+ ...result,
201
+ error: {
202
+ ...result.error,
203
+ details: {
204
+ ...isRecord(result.error.details) ? result.error.details : {},
205
+ approvalUrl: this.#connectUrl(pending2),
206
+ expiresAt: pending2.expiresAt
207
+ }
208
+ }
209
+ };
210
+ }
211
+ if (result.status !== "authority_required") return result;
180
212
  const request = isRecord(result.request) ? result.request : void 0;
181
213
  const requestId = typeof request?.requestId === "string" ? request.requestId : void 0;
182
214
  const requested = Array.isArray(request?.requested) ? request.requested.filter(isPermissionEntry) : [];
@@ -189,7 +221,7 @@ var RemoteTenantStore = class {
189
221
  kind: "openkey",
190
222
  requestId,
191
223
  url: this.#connectUrl(pending),
192
- fallback: "Open the approval URL, then retry the same MCP tool."
224
+ fallback: (0, import_tc_apps.tcAppsApprovalMessage)(requested) ?? "Open the approval URL, then retry the same MCP tool."
193
225
  }
194
226
  };
195
227
  }
@@ -206,7 +238,7 @@ var RemoteTenantStore = class {
206
238
  permissions: Buffer.from(JSON.stringify({
207
239
  permissions: pending.permissions,
208
240
  reason: [
209
- pending.kind === "bootstrap" ? "Connect this TinyCloud account to the hosted MCP delegate." : "Approve the exact capabilities requested by the TinyCloud MCP operation.",
241
+ pending.kind === "bootstrap" ? "Connect this TinyCloud account to the hosted MCP delegate." : (0, import_tc_apps.tcAppsApprovalReason)(pending.permissions) ?? "Approve the exact capabilities requested by the TinyCloud MCP operation.",
210
242
  // Owner DIDs come from the verified OpenKey access token of the MCP
211
243
  // client that created this link, never from the link itself.
212
244
  ...pending.ownerDids.length === 0 ? [] : [`Requested by TinyCloud account ${pending.ownerDids.join(", ")}; approve only if that is you.`]
@@ -634,15 +666,37 @@ var import_ajv = require("@modelcontextprotocol/server/validators/ajv");
634
666
  var import_operations2 = require("@tinycloud/operations");
635
667
 
636
668
  // src/results.ts
637
- function toMcpToolResult(result, bindings = []) {
638
- const canonical = isCanonicalOperationResult(result) ? withRecoveryTool(result, bindings) : {
669
+ function toMcpToolResult(result, bindings = [], options = {}) {
670
+ let canonical = isCanonicalOperationResult(result) ? withRecoveryTool(result, bindings) : {
639
671
  status: "error",
640
672
  operation: {},
641
673
  context: {},
642
674
  error: { code: "INTERNAL_ERROR" }
643
675
  };
676
+ let serialized;
677
+ if (options.surface === "hosted") {
678
+ serialized = JSON.stringify(canonical);
679
+ if (options.readOnly === true && serialized.length > 1e5) {
680
+ const sql = options.operationId?.startsWith("tinycloud.sql.") || options.operationId === "tinycloud.apps.query";
681
+ canonical = {
682
+ status: "error",
683
+ operation: canonical.operation,
684
+ context: canonical.context,
685
+ error: {
686
+ code: sql ? "SQL_RESULT_LIMIT_EXCEEDED" : "KV_RESPONSE_TOO_LARGE",
687
+ message: sql ? "The result exceeds 100,000 characters. Select fewer columns or rows, add a WHERE filter or a smaller LIMIT, or aggregate the data." : "The result exceeds 100,000 characters. Request fewer keys, narrow the prefix, or use a smaller page size.",
688
+ retryable: false
689
+ }
690
+ };
691
+ serialized = JSON.stringify(canonical);
692
+ }
693
+ }
644
694
  return {
645
- content: [{ type: "text", text: resultSummary(canonical.status) }],
695
+ content: [{
696
+ type: "text",
697
+ text: resultSummary(canonical.status) + (serialized !== void 0 && !(options.operationId === "tinycloud.secrets.get" && canonical.status === "ok") ? `
698
+ ${serialized}` : "")
699
+ }],
646
700
  structuredContent: canonical
647
701
  };
648
702
  }
@@ -691,6 +745,9 @@ var packageRequire = (0, import_node_module.createRequire)(
691
745
  var generatedCatalog = packageRequire(
692
746
  "@tinycloud/operations/operations.json"
693
747
  );
748
+ var stableErrorCodes = Object.fromEntries(
749
+ generatedCatalog.stableErrors.map((code) => [code, true])
750
+ );
694
751
  var ALL_SURFACES = ["local", "hosted"];
695
752
  function bindingSurfaces(binding) {
696
753
  return binding.surfaces ?? ALL_SURFACES;
@@ -854,6 +911,46 @@ var TOOL_BINDINGS = [
854
911
  // Hosted has no persistent reauthorization path for a revoked session
855
912
  // yet (TC-896), so status is local/stdio only.
856
913
  surfaces: ["local"]
914
+ },
915
+ {
916
+ name: "tinycloud_apps_context",
917
+ operationId: "tinycloud.apps.context",
918
+ operationVersion: 1,
919
+ readOnlyHint: true,
920
+ idempotentHint: true,
921
+ destructiveHint: false,
922
+ openWorldHint: true,
923
+ surfaces: ["hosted"]
924
+ },
925
+ {
926
+ name: "tinycloud_apps_read",
927
+ operationId: "tinycloud.apps.read",
928
+ operationVersion: 1,
929
+ readOnlyHint: true,
930
+ idempotentHint: true,
931
+ destructiveHint: false,
932
+ openWorldHint: true,
933
+ surfaces: ["hosted"]
934
+ },
935
+ {
936
+ name: "tinycloud_apps_write",
937
+ operationId: "tinycloud.apps.write",
938
+ operationVersion: 1,
939
+ readOnlyHint: false,
940
+ idempotentHint: false,
941
+ destructiveHint: true,
942
+ openWorldHint: true,
943
+ surfaces: ["hosted"]
944
+ },
945
+ {
946
+ name: "tinycloud_apps_query",
947
+ operationId: "tinycloud.apps.query",
948
+ operationVersion: 1,
949
+ readOnlyHint: true,
950
+ idempotentHint: true,
951
+ destructiveHint: false,
952
+ openWorldHint: true,
953
+ surfaces: ["hosted"]
857
954
  }
858
955
  ];
859
956
  function createJsonSchemaValidator() {
@@ -898,23 +995,50 @@ function registerTinyCloudTools(server, selection, validator) {
898
995
  annotations
899
996
  },
900
997
  async (input) => {
901
- const result = await (0, import_operations2.invokeOperation)(
902
- binding.operationId,
903
- binding.operationVersion,
904
- invocationTargetForMcp(selection),
905
- input
906
- );
907
- const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
908
- return toMcpToolResult(projected, TOOL_BINDINGS);
998
+ const startedAt = performance.now();
999
+ const metrics = { replayedGrants: 0 };
1000
+ let code = "INTERNAL_ERROR";
1001
+ try {
1002
+ const result = await (0, import_operations2.invokeOperation)(
1003
+ binding.operationId,
1004
+ binding.operationVersion,
1005
+ invocationTargetForMcp(selection),
1006
+ input,
1007
+ metrics
1008
+ );
1009
+ const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
1010
+ const response = toMcpToolResult(projected, TOOL_BINDINGS, {
1011
+ surface: selection.surface,
1012
+ readOnly: binding.readOnlyHint,
1013
+ operationId: binding.operationId
1014
+ });
1015
+ code = outcomeCode(response.structuredContent);
1016
+ return response;
1017
+ } finally {
1018
+ if (selection.surface === "hosted") {
1019
+ logToolCall(binding.name, code, startedAt, metrics.replayedGrants);
1020
+ }
1021
+ }
909
1022
  }
910
1023
  );
911
1024
  }
912
1025
  }
1026
+ function outcomeCode(result) {
1027
+ if (result.status === "error") {
1028
+ const error = result.error;
1029
+ return typeof error?.code === "string" && Object.hasOwn(stableErrorCodes, error.code) ? error.code : "INTERNAL_ERROR";
1030
+ }
1031
+ if (result.status !== "ok") return result.status;
1032
+ const output = result.output;
1033
+ if (output?.outcome === "COPY_PARTIAL") return "COPY_PARTIAL";
1034
+ if (Array.isArray(output?.ops) && output.ops.some((op) => op.outcome !== "ok")) return "PARTIAL";
1035
+ return "ok";
1036
+ }
913
1037
 
914
1038
  // package.json
915
1039
  var package_default = {
916
1040
  name: "@tinycloud/mcp",
917
- version: "0.5.5-beta.0",
1041
+ version: "0.6.0-beta.2",
918
1042
  description: "TinyCloud delegated operations MCP server",
919
1043
  keywords: [
920
1044
  "tinycloud",
@@ -987,7 +1111,7 @@ var package_default = {
987
1111
  "@modelcontextprotocol/server": "2.0.0-beta.4",
988
1112
  "@tinycloud/node-sdk": "3.2.5-beta.0",
989
1113
  "@tinycloud/node-sdk-wasm": "1.7.7",
990
- "@tinycloud/operations": "0.5.5-beta.0",
1114
+ "@tinycloud/operations": "0.6.0-beta.2",
991
1115
  jose: "6.1.3"
992
1116
  },
993
1117
  devDependencies: {
@@ -1031,13 +1155,13 @@ function createHostedMcpApp(config) {
1031
1155
  const handler = (0, import_server4.createMcpHandler)(async ({ authInfo: authInfo2 }) => {
1032
1156
  const identity = oauthIdentity(authInfo2);
1033
1157
  const { subject, ownerDids } = identity;
1034
- const stateRoot = store.tenantStateRoot(subject);
1158
+ const stateRoot = await store.initializeTenant(subject);
1035
1159
  const validator = createJsonSchemaValidator();
1036
1160
  const server = new import_server4.McpServer(
1037
1161
  { name: MCP_SERVER_NAME, version: MCP_VERSION },
1038
1162
  {
1039
1163
  jsonSchemaValidator: validator,
1040
- instructions: "Call tinycloud_connect first. If an operation returns authority_required, open approval.url, then retry that operation unchanged."
1164
+ 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."
1041
1165
  }
1042
1166
  );
1043
1167
  registerTinyCloudTools(server, {
@@ -1149,14 +1273,21 @@ function registerConnectTool(server, validator, store, identity) {
1149
1273
  }
1150
1274
  },
1151
1275
  async () => {
1152
- const status = await store.connectStatus(identity.subject, identity.ownerDids);
1153
- return {
1154
- content: [{
1155
- type: "text",
1156
- text: status.connected ? "TinyCloud is connected." : "TinyCloud approval is required; open approvalUrl, then call this tool again."
1157
- }],
1158
- structuredContent: status
1159
- };
1276
+ const startedAt = performance.now();
1277
+ let code = "INTERNAL_ERROR";
1278
+ try {
1279
+ const status = await store.connectStatus(identity.subject, identity.ownerDids);
1280
+ code = status.connected ? "ok" : "authority_required";
1281
+ return {
1282
+ content: [{
1283
+ type: "text",
1284
+ text: status.connected ? "TinyCloud is connected." : `TinyCloud approval is required. Open ${status.approvalUrl}, then call this tool again. This link expires at ${status.expiresAt}.`
1285
+ }],
1286
+ structuredContent: status
1287
+ };
1288
+ } finally {
1289
+ logToolCall("tinycloud_connect", code, startedAt, 0);
1290
+ }
1160
1291
  }
1161
1292
  );
1162
1293
  }