@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.js CHANGED
@@ -90,6 +90,17 @@ function firstString(...values) {
90
90
  return values.find((value) => typeof value === "string" && value.length > 0);
91
91
  }
92
92
 
93
+ // src/logging.ts
94
+ function logToolCall(tool, code, startedAt, replayedGrants) {
95
+ process.stderr.write(`${JSON.stringify({
96
+ tool,
97
+ code,
98
+ durationMs: Math.max(0, Math.round(performance.now() - startedAt)),
99
+ replayedGrants
100
+ })}
101
+ `);
102
+ }
103
+
93
104
  // src/remote-store.ts
94
105
  import { createHmac, randomBytes, timingSafeEqual } from "crypto";
95
106
  import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
@@ -105,6 +116,7 @@ import {
105
116
  } from "@tinycloud/node-sdk";
106
117
  import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk-wasm";
107
118
  import { invokeOperation } from "@tinycloud/operations";
119
+ import { tcAppsApprovalMessage, tcAppsApprovalReason } from "@tinycloud/operations/tc-apps";
108
120
  var PROFILE = "agent";
109
121
  var MAX_CALLBACK_BYTES = 256 * 1024;
110
122
  var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
@@ -157,6 +169,10 @@ var RemoteTenantStore = class {
157
169
  tenantStateRoot(subject) {
158
170
  return join(this.#config.stateDir, "tenants", this.#tenantId(subject));
159
171
  }
172
+ /** Prepare only this authenticated account's profile before its first operation. */
173
+ async initializeTenant(subject) {
174
+ return (await this.#ensureTenant(subject)).stateRoot;
175
+ }
160
176
  async connectStatus(subject, ownerDids = []) {
161
177
  const tenant = await this.#ensureTenant(subject);
162
178
  if (await this.#hasValidSession(tenant, ownerDids)) {
@@ -171,7 +187,23 @@ var RemoteTenantStore = class {
171
187
  };
172
188
  }
173
189
  async decorateAuthorityResult(subject, ownerDids, result) {
174
- if (!isRecord(result) || result.status !== "authority_required") return result;
190
+ if (!isRecord(result)) return result;
191
+ if (result.status === "error" && isRecord(result.error) && (result.error.code === "SESSION_NOT_FOUND" || result.error.code === "AUTH_REQUIRED")) {
192
+ await this.#ensureTenant(subject);
193
+ const pending2 = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
194
+ return {
195
+ ...result,
196
+ error: {
197
+ ...result.error,
198
+ details: {
199
+ ...isRecord(result.error.details) ? result.error.details : {},
200
+ approvalUrl: this.#connectUrl(pending2),
201
+ expiresAt: pending2.expiresAt
202
+ }
203
+ }
204
+ };
205
+ }
206
+ if (result.status !== "authority_required") return result;
175
207
  const request = isRecord(result.request) ? result.request : void 0;
176
208
  const requestId = typeof request?.requestId === "string" ? request.requestId : void 0;
177
209
  const requested = Array.isArray(request?.requested) ? request.requested.filter(isPermissionEntry) : [];
@@ -184,7 +216,7 @@ var RemoteTenantStore = class {
184
216
  kind: "openkey",
185
217
  requestId,
186
218
  url: this.#connectUrl(pending),
187
- fallback: "Open the approval URL, then retry the same MCP tool."
219
+ fallback: tcAppsApprovalMessage(requested) ?? "Open the approval URL, then retry the same MCP tool."
188
220
  }
189
221
  };
190
222
  }
@@ -201,7 +233,7 @@ var RemoteTenantStore = class {
201
233
  permissions: Buffer.from(JSON.stringify({
202
234
  permissions: pending.permissions,
203
235
  reason: [
204
- pending.kind === "bootstrap" ? "Connect this TinyCloud account to the hosted MCP delegate." : "Approve the exact capabilities requested by the TinyCloud MCP operation.",
236
+ pending.kind === "bootstrap" ? "Connect this TinyCloud account to the hosted MCP delegate." : tcAppsApprovalReason(pending.permissions) ?? "Approve the exact capabilities requested by the TinyCloud MCP operation.",
205
237
  // Owner DIDs come from the verified OpenKey access token of the MCP
206
238
  // client that created this link, never from the link itself.
207
239
  ...pending.ownerDids.length === 0 ? [] : [`Requested by TinyCloud account ${pending.ownerDids.join(", ")}; approve only if that is you.`]
@@ -629,15 +661,37 @@ import { addFormats, Ajv, AjvJsonSchemaValidator } from "@modelcontextprotocol/s
629
661
  import { invokeOperation as invokeOperation2 } from "@tinycloud/operations";
630
662
 
631
663
  // src/results.ts
632
- function toMcpToolResult(result, bindings = []) {
633
- const canonical = isCanonicalOperationResult(result) ? withRecoveryTool(result, bindings) : {
664
+ function toMcpToolResult(result, bindings = [], options = {}) {
665
+ let canonical = isCanonicalOperationResult(result) ? withRecoveryTool(result, bindings) : {
634
666
  status: "error",
635
667
  operation: {},
636
668
  context: {},
637
669
  error: { code: "INTERNAL_ERROR" }
638
670
  };
671
+ let serialized;
672
+ if (options.surface === "hosted") {
673
+ serialized = JSON.stringify(canonical);
674
+ if (options.readOnly === true && serialized.length > 1e5) {
675
+ const sql = options.operationId?.startsWith("tinycloud.sql.") || options.operationId === "tinycloud.apps.query";
676
+ canonical = {
677
+ status: "error",
678
+ operation: canonical.operation,
679
+ context: canonical.context,
680
+ error: {
681
+ code: sql ? "SQL_RESULT_LIMIT_EXCEEDED" : "KV_RESPONSE_TOO_LARGE",
682
+ 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.",
683
+ retryable: false
684
+ }
685
+ };
686
+ serialized = JSON.stringify(canonical);
687
+ }
688
+ }
639
689
  return {
640
- content: [{ type: "text", text: resultSummary(canonical.status) }],
690
+ content: [{
691
+ type: "text",
692
+ text: resultSummary(canonical.status) + (serialized !== void 0 && !(options.operationId === "tinycloud.secrets.get" && canonical.status === "ok") ? `
693
+ ${serialized}` : "")
694
+ }],
641
695
  structuredContent: canonical
642
696
  };
643
697
  }
@@ -685,6 +739,9 @@ var packageRequire = createRequire(
685
739
  var generatedCatalog = packageRequire(
686
740
  "@tinycloud/operations/operations.json"
687
741
  );
742
+ var stableErrorCodes = Object.fromEntries(
743
+ generatedCatalog.stableErrors.map((code) => [code, true])
744
+ );
688
745
  var ALL_SURFACES = ["local", "hosted"];
689
746
  function bindingSurfaces(binding) {
690
747
  return binding.surfaces ?? ALL_SURFACES;
@@ -848,6 +905,36 @@ var TOOL_BINDINGS = [
848
905
  // Hosted has no persistent reauthorization path for a revoked session
849
906
  // yet (TC-896), so status is local/stdio only.
850
907
  surfaces: ["local"]
908
+ },
909
+ {
910
+ name: "tinycloud_apps_context",
911
+ operationId: "tinycloud.apps.context",
912
+ operationVersion: 1,
913
+ readOnlyHint: true,
914
+ idempotentHint: true,
915
+ destructiveHint: false,
916
+ openWorldHint: true,
917
+ surfaces: ["hosted"]
918
+ },
919
+ {
920
+ name: "tinycloud_apps_read",
921
+ operationId: "tinycloud.apps.read",
922
+ operationVersion: 1,
923
+ readOnlyHint: true,
924
+ idempotentHint: true,
925
+ destructiveHint: false,
926
+ openWorldHint: true,
927
+ surfaces: ["hosted"]
928
+ },
929
+ {
930
+ name: "tinycloud_apps_write",
931
+ operationId: "tinycloud.apps.write",
932
+ operationVersion: 1,
933
+ readOnlyHint: false,
934
+ idempotentHint: false,
935
+ destructiveHint: true,
936
+ openWorldHint: true,
937
+ surfaces: ["hosted"]
851
938
  }
852
939
  ];
853
940
  function createJsonSchemaValidator() {
@@ -892,23 +979,50 @@ function registerTinyCloudTools(server, selection, validator) {
892
979
  annotations
893
980
  },
894
981
  async (input) => {
895
- const result = await invokeOperation2(
896
- binding.operationId,
897
- binding.operationVersion,
898
- invocationTargetForMcp(selection),
899
- input
900
- );
901
- const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
902
- return toMcpToolResult(projected, TOOL_BINDINGS);
982
+ const startedAt = performance.now();
983
+ const metrics = { replayedGrants: 0 };
984
+ let code = "INTERNAL_ERROR";
985
+ try {
986
+ const result = await invokeOperation2(
987
+ binding.operationId,
988
+ binding.operationVersion,
989
+ invocationTargetForMcp(selection),
990
+ input,
991
+ metrics
992
+ );
993
+ const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
994
+ const response = toMcpToolResult(projected, TOOL_BINDINGS, {
995
+ surface: selection.surface,
996
+ readOnly: binding.readOnlyHint,
997
+ operationId: binding.operationId
998
+ });
999
+ code = outcomeCode(response.structuredContent);
1000
+ return response;
1001
+ } finally {
1002
+ if (selection.surface === "hosted") {
1003
+ logToolCall(binding.name, code, startedAt, metrics.replayedGrants);
1004
+ }
1005
+ }
903
1006
  }
904
1007
  );
905
1008
  }
906
1009
  }
1010
+ function outcomeCode(result) {
1011
+ if (result.status === "error") {
1012
+ const error = result.error;
1013
+ return typeof error?.code === "string" && Object.hasOwn(stableErrorCodes, error.code) ? error.code : "INTERNAL_ERROR";
1014
+ }
1015
+ if (result.status !== "ok") return result.status;
1016
+ const output = result.output;
1017
+ if (output?.outcome === "COPY_PARTIAL") return "COPY_PARTIAL";
1018
+ if (Array.isArray(output?.ops) && output.ops.some((op) => op.outcome !== "ok")) return "PARTIAL";
1019
+ return "ok";
1020
+ }
907
1021
 
908
1022
  // package.json
909
1023
  var package_default = {
910
1024
  name: "@tinycloud/mcp",
911
- version: "0.5.5-beta.0",
1025
+ version: "0.6.0-beta.1",
912
1026
  description: "TinyCloud delegated operations MCP server",
913
1027
  keywords: [
914
1028
  "tinycloud",
@@ -981,7 +1095,7 @@ var package_default = {
981
1095
  "@modelcontextprotocol/server": "2.0.0-beta.4",
982
1096
  "@tinycloud/node-sdk": "3.2.5-beta.0",
983
1097
  "@tinycloud/node-sdk-wasm": "1.7.7",
984
- "@tinycloud/operations": "0.5.5-beta.0",
1098
+ "@tinycloud/operations": "0.6.0-beta.1",
985
1099
  jose: "6.1.3"
986
1100
  },
987
1101
  devDependencies: {
@@ -1025,13 +1139,13 @@ function createHostedMcpApp(config) {
1025
1139
  const handler = createMcpHandler(async ({ authInfo: authInfo2 }) => {
1026
1140
  const identity = oauthIdentity(authInfo2);
1027
1141
  const { subject, ownerDids } = identity;
1028
- const stateRoot = store.tenantStateRoot(subject);
1142
+ const stateRoot = await store.initializeTenant(subject);
1029
1143
  const validator = createJsonSchemaValidator();
1030
1144
  const server = new McpServer2(
1031
1145
  { name: MCP_SERVER_NAME, version: MCP_VERSION },
1032
1146
  {
1033
1147
  jsonSchemaValidator: validator,
1034
- instructions: "Call tinycloud_connect first. If an operation returns authority_required, open approval.url, then retry that operation unchanged."
1148
+ 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."
1035
1149
  }
1036
1150
  );
1037
1151
  registerTinyCloudTools(server, {
@@ -1143,14 +1257,21 @@ function registerConnectTool(server, validator, store, identity) {
1143
1257
  }
1144
1258
  },
1145
1259
  async () => {
1146
- const status = await store.connectStatus(identity.subject, identity.ownerDids);
1147
- return {
1148
- content: [{
1149
- type: "text",
1150
- text: status.connected ? "TinyCloud is connected." : "TinyCloud approval is required; open approvalUrl, then call this tool again."
1151
- }],
1152
- structuredContent: status
1153
- };
1260
+ const startedAt = performance.now();
1261
+ let code = "INTERNAL_ERROR";
1262
+ try {
1263
+ const status = await store.connectStatus(identity.subject, identity.ownerDids);
1264
+ code = status.connected ? "ok" : "authority_required";
1265
+ return {
1266
+ content: [{
1267
+ type: "text",
1268
+ text: status.connected ? "TinyCloud is connected." : `TinyCloud approval is required. Open ${status.approvalUrl}, then call this tool again. This link expires at ${status.expiresAt}.`
1269
+ }],
1270
+ structuredContent: status
1271
+ };
1272
+ } finally {
1273
+ logToolCall("tinycloud_connect", code, startedAt, 0);
1274
+ }
1154
1275
  }
1155
1276
  );
1156
1277
  }