@tinycloud/mcp 0.5.5-beta.0 → 0.6.0-beta.1

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,36 @@ 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"]
857
944
  }
858
945
  ];
859
946
  function createJsonSchemaValidator() {
@@ -898,23 +985,50 @@ function registerTinyCloudTools(server, selection, validator) {
898
985
  annotations
899
986
  },
900
987
  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);
988
+ const startedAt = performance.now();
989
+ const metrics = { replayedGrants: 0 };
990
+ let code = "INTERNAL_ERROR";
991
+ try {
992
+ const result = await (0, import_operations2.invokeOperation)(
993
+ binding.operationId,
994
+ binding.operationVersion,
995
+ invocationTargetForMcp(selection),
996
+ input,
997
+ metrics
998
+ );
999
+ const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
1000
+ const response = toMcpToolResult(projected, TOOL_BINDINGS, {
1001
+ surface: selection.surface,
1002
+ readOnly: binding.readOnlyHint,
1003
+ operationId: binding.operationId
1004
+ });
1005
+ code = outcomeCode(response.structuredContent);
1006
+ return response;
1007
+ } finally {
1008
+ if (selection.surface === "hosted") {
1009
+ logToolCall(binding.name, code, startedAt, metrics.replayedGrants);
1010
+ }
1011
+ }
909
1012
  }
910
1013
  );
911
1014
  }
912
1015
  }
1016
+ function outcomeCode(result) {
1017
+ if (result.status === "error") {
1018
+ const error = result.error;
1019
+ return typeof error?.code === "string" && Object.hasOwn(stableErrorCodes, error.code) ? error.code : "INTERNAL_ERROR";
1020
+ }
1021
+ if (result.status !== "ok") return result.status;
1022
+ const output = result.output;
1023
+ if (output?.outcome === "COPY_PARTIAL") return "COPY_PARTIAL";
1024
+ if (Array.isArray(output?.ops) && output.ops.some((op) => op.outcome !== "ok")) return "PARTIAL";
1025
+ return "ok";
1026
+ }
913
1027
 
914
1028
  // package.json
915
1029
  var package_default = {
916
1030
  name: "@tinycloud/mcp",
917
- version: "0.5.5-beta.0",
1031
+ version: "0.6.0-beta.1",
918
1032
  description: "TinyCloud delegated operations MCP server",
919
1033
  keywords: [
920
1034
  "tinycloud",
@@ -987,7 +1101,7 @@ var package_default = {
987
1101
  "@modelcontextprotocol/server": "2.0.0-beta.4",
988
1102
  "@tinycloud/node-sdk": "3.2.5-beta.0",
989
1103
  "@tinycloud/node-sdk-wasm": "1.7.7",
990
- "@tinycloud/operations": "0.5.5-beta.0",
1104
+ "@tinycloud/operations": "0.6.0-beta.1",
991
1105
  jose: "6.1.3"
992
1106
  },
993
1107
  devDependencies: {
@@ -1031,13 +1145,13 @@ function createHostedMcpApp(config) {
1031
1145
  const handler = (0, import_server4.createMcpHandler)(async ({ authInfo: authInfo2 }) => {
1032
1146
  const identity = oauthIdentity(authInfo2);
1033
1147
  const { subject, ownerDids } = identity;
1034
- const stateRoot = store.tenantStateRoot(subject);
1148
+ const stateRoot = await store.initializeTenant(subject);
1035
1149
  const validator = createJsonSchemaValidator();
1036
1150
  const server = new import_server4.McpServer(
1037
1151
  { name: MCP_SERVER_NAME, version: MCP_VERSION },
1038
1152
  {
1039
1153
  jsonSchemaValidator: validator,
1040
- instructions: "Call tinycloud_connect first. If an operation returns authority_required, open approval.url, then retry that operation unchanged."
1154
+ 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
1155
  }
1042
1156
  );
1043
1157
  registerTinyCloudTools(server, {
@@ -1149,14 +1263,21 @@ function registerConnectTool(server, validator, store, identity) {
1149
1263
  }
1150
1264
  },
1151
1265
  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
- };
1266
+ const startedAt = performance.now();
1267
+ let code = "INTERNAL_ERROR";
1268
+ try {
1269
+ const status = await store.connectStatus(identity.subject, identity.ownerDids);
1270
+ code = status.connected ? "ok" : "authority_required";
1271
+ return {
1272
+ content: [{
1273
+ type: "text",
1274
+ text: status.connected ? "TinyCloud is connected." : `TinyCloud approval is required. Open ${status.approvalUrl}, then call this tool again. This link expires at ${status.expiresAt}.`
1275
+ }],
1276
+ structuredContent: status
1277
+ };
1278
+ } finally {
1279
+ logToolCall("tinycloud_connect", code, startedAt, 0);
1280
+ }
1160
1281
  }
1161
1282
  );
1162
1283
  }