@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.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,46 @@ 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"]
938
+ },
939
+ {
940
+ name: "tinycloud_apps_query",
941
+ operationId: "tinycloud.apps.query",
942
+ operationVersion: 1,
943
+ readOnlyHint: true,
944
+ idempotentHint: true,
945
+ destructiveHint: false,
946
+ openWorldHint: true,
947
+ surfaces: ["hosted"]
851
948
  }
852
949
  ];
853
950
  function createJsonSchemaValidator() {
@@ -892,23 +989,50 @@ function registerTinyCloudTools(server, selection, validator) {
892
989
  annotations
893
990
  },
894
991
  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);
992
+ const startedAt = performance.now();
993
+ const metrics = { replayedGrants: 0 };
994
+ let code = "INTERNAL_ERROR";
995
+ try {
996
+ const result = await invokeOperation2(
997
+ binding.operationId,
998
+ binding.operationVersion,
999
+ invocationTargetForMcp(selection),
1000
+ input,
1001
+ metrics
1002
+ );
1003
+ const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
1004
+ const response = toMcpToolResult(projected, TOOL_BINDINGS, {
1005
+ surface: selection.surface,
1006
+ readOnly: binding.readOnlyHint,
1007
+ operationId: binding.operationId
1008
+ });
1009
+ code = outcomeCode(response.structuredContent);
1010
+ return response;
1011
+ } finally {
1012
+ if (selection.surface === "hosted") {
1013
+ logToolCall(binding.name, code, startedAt, metrics.replayedGrants);
1014
+ }
1015
+ }
903
1016
  }
904
1017
  );
905
1018
  }
906
1019
  }
1020
+ function outcomeCode(result) {
1021
+ if (result.status === "error") {
1022
+ const error = result.error;
1023
+ return typeof error?.code === "string" && Object.hasOwn(stableErrorCodes, error.code) ? error.code : "INTERNAL_ERROR";
1024
+ }
1025
+ if (result.status !== "ok") return result.status;
1026
+ const output = result.output;
1027
+ if (output?.outcome === "COPY_PARTIAL") return "COPY_PARTIAL";
1028
+ if (Array.isArray(output?.ops) && output.ops.some((op) => op.outcome !== "ok")) return "PARTIAL";
1029
+ return "ok";
1030
+ }
907
1031
 
908
1032
  // package.json
909
1033
  var package_default = {
910
1034
  name: "@tinycloud/mcp",
911
- version: "0.5.5-beta.0",
1035
+ version: "0.6.0-beta.2",
912
1036
  description: "TinyCloud delegated operations MCP server",
913
1037
  keywords: [
914
1038
  "tinycloud",
@@ -981,7 +1105,7 @@ var package_default = {
981
1105
  "@modelcontextprotocol/server": "2.0.0-beta.4",
982
1106
  "@tinycloud/node-sdk": "3.2.5-beta.0",
983
1107
  "@tinycloud/node-sdk-wasm": "1.7.7",
984
- "@tinycloud/operations": "0.5.5-beta.0",
1108
+ "@tinycloud/operations": "0.6.0-beta.2",
985
1109
  jose: "6.1.3"
986
1110
  },
987
1111
  devDependencies: {
@@ -1025,13 +1149,13 @@ function createHostedMcpApp(config) {
1025
1149
  const handler = createMcpHandler(async ({ authInfo: authInfo2 }) => {
1026
1150
  const identity = oauthIdentity(authInfo2);
1027
1151
  const { subject, ownerDids } = identity;
1028
- const stateRoot = store.tenantStateRoot(subject);
1152
+ const stateRoot = await store.initializeTenant(subject);
1029
1153
  const validator = createJsonSchemaValidator();
1030
1154
  const server = new McpServer2(
1031
1155
  { name: MCP_SERVER_NAME, version: MCP_VERSION },
1032
1156
  {
1033
1157
  jsonSchemaValidator: validator,
1034
- instructions: "Call tinycloud_connect first. If an operation returns authority_required, open approval.url, then retry that operation unchanged."
1158
+ 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
1159
  }
1036
1160
  );
1037
1161
  registerTinyCloudTools(server, {
@@ -1143,14 +1267,21 @@ function registerConnectTool(server, validator, store, identity) {
1143
1267
  }
1144
1268
  },
1145
1269
  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
- };
1270
+ const startedAt = performance.now();
1271
+ let code = "INTERNAL_ERROR";
1272
+ try {
1273
+ const status = await store.connectStatus(identity.subject, identity.ownerDids);
1274
+ code = status.connected ? "ok" : "authority_required";
1275
+ return {
1276
+ content: [{
1277
+ type: "text",
1278
+ text: status.connected ? "TinyCloud is connected." : `TinyCloud approval is required. Open ${status.approvalUrl}, then call this tool again. This link expires at ${status.expiresAt}.`
1279
+ }],
1280
+ structuredContent: status
1281
+ };
1282
+ } finally {
1283
+ logToolCall("tinycloud_connect", code, startedAt, 0);
1284
+ }
1154
1285
  }
1155
1286
  );
1156
1287
  }