@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-cli.js CHANGED
@@ -107,6 +107,17 @@ function firstString(...values) {
107
107
  return values.find((value) => typeof value === "string" && value.length > 0);
108
108
  }
109
109
 
110
+ // src/logging.ts
111
+ function logToolCall(tool, code, startedAt, replayedGrants) {
112
+ process.stderr.write(`${JSON.stringify({
113
+ tool,
114
+ code,
115
+ durationMs: Math.max(0, Math.round(performance.now() - startedAt)),
116
+ replayedGrants
117
+ })}
118
+ `);
119
+ }
120
+
110
121
  // src/remote-store.ts
111
122
  import { createHmac, randomBytes, timingSafeEqual } from "crypto";
112
123
  import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
@@ -122,6 +133,7 @@ import {
122
133
  } from "@tinycloud/node-sdk";
123
134
  import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk-wasm";
124
135
  import { invokeOperation } from "@tinycloud/operations";
136
+ import { tcAppsApprovalMessage, tcAppsApprovalReason } from "@tinycloud/operations/tc-apps";
125
137
  var PROFILE = "agent";
126
138
  var MAX_CALLBACK_BYTES = 256 * 1024;
127
139
  var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
@@ -174,6 +186,10 @@ var RemoteTenantStore = class {
174
186
  tenantStateRoot(subject) {
175
187
  return join(this.#config.stateDir, "tenants", this.#tenantId(subject));
176
188
  }
189
+ /** Prepare only this authenticated account's profile before its first operation. */
190
+ async initializeTenant(subject) {
191
+ return (await this.#ensureTenant(subject)).stateRoot;
192
+ }
177
193
  async connectStatus(subject, ownerDids = []) {
178
194
  const tenant = await this.#ensureTenant(subject);
179
195
  if (await this.#hasValidSession(tenant, ownerDids)) {
@@ -188,7 +204,23 @@ var RemoteTenantStore = class {
188
204
  };
189
205
  }
190
206
  async decorateAuthorityResult(subject, ownerDids, result) {
191
- if (!isRecord(result) || result.status !== "authority_required") return result;
207
+ if (!isRecord(result)) return result;
208
+ if (result.status === "error" && isRecord(result.error) && (result.error.code === "SESSION_NOT_FOUND" || result.error.code === "AUTH_REQUIRED")) {
209
+ await this.#ensureTenant(subject);
210
+ const pending2 = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
211
+ return {
212
+ ...result,
213
+ error: {
214
+ ...result.error,
215
+ details: {
216
+ ...isRecord(result.error.details) ? result.error.details : {},
217
+ approvalUrl: this.#connectUrl(pending2),
218
+ expiresAt: pending2.expiresAt
219
+ }
220
+ }
221
+ };
222
+ }
223
+ if (result.status !== "authority_required") return result;
192
224
  const request = isRecord(result.request) ? result.request : void 0;
193
225
  const requestId = typeof request?.requestId === "string" ? request.requestId : void 0;
194
226
  const requested = Array.isArray(request?.requested) ? request.requested.filter(isPermissionEntry) : [];
@@ -201,7 +233,7 @@ var RemoteTenantStore = class {
201
233
  kind: "openkey",
202
234
  requestId,
203
235
  url: this.#connectUrl(pending),
204
- fallback: "Open the approval URL, then retry the same MCP tool."
236
+ fallback: tcAppsApprovalMessage(requested) ?? "Open the approval URL, then retry the same MCP tool."
205
237
  }
206
238
  };
207
239
  }
@@ -218,7 +250,7 @@ var RemoteTenantStore = class {
218
250
  permissions: Buffer.from(JSON.stringify({
219
251
  permissions: pending.permissions,
220
252
  reason: [
221
- pending.kind === "bootstrap" ? "Connect this TinyCloud account to the hosted MCP delegate." : "Approve the exact capabilities requested by the TinyCloud MCP operation.",
253
+ 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.",
222
254
  // Owner DIDs come from the verified OpenKey access token of the MCP
223
255
  // client that created this link, never from the link itself.
224
256
  ...pending.ownerDids.length === 0 ? [] : [`Requested by TinyCloud account ${pending.ownerDids.join(", ")}; approve only if that is you.`]
@@ -646,15 +678,37 @@ import { addFormats, Ajv, AjvJsonSchemaValidator } from "@modelcontextprotocol/s
646
678
  import { invokeOperation as invokeOperation2 } from "@tinycloud/operations";
647
679
 
648
680
  // src/results.ts
649
- function toMcpToolResult(result, bindings = []) {
650
- const canonical = isCanonicalOperationResult(result) ? withRecoveryTool(result, bindings) : {
681
+ function toMcpToolResult(result, bindings = [], options = {}) {
682
+ let canonical = isCanonicalOperationResult(result) ? withRecoveryTool(result, bindings) : {
651
683
  status: "error",
652
684
  operation: {},
653
685
  context: {},
654
686
  error: { code: "INTERNAL_ERROR" }
655
687
  };
688
+ let serialized;
689
+ if (options.surface === "hosted") {
690
+ serialized = JSON.stringify(canonical);
691
+ if (options.readOnly === true && serialized.length > 1e5) {
692
+ const sql = options.operationId?.startsWith("tinycloud.sql.") || options.operationId === "tinycloud.apps.query";
693
+ canonical = {
694
+ status: "error",
695
+ operation: canonical.operation,
696
+ context: canonical.context,
697
+ error: {
698
+ code: sql ? "SQL_RESULT_LIMIT_EXCEEDED" : "KV_RESPONSE_TOO_LARGE",
699
+ 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.",
700
+ retryable: false
701
+ }
702
+ };
703
+ serialized = JSON.stringify(canonical);
704
+ }
705
+ }
656
706
  return {
657
- content: [{ type: "text", text: resultSummary(canonical.status) }],
707
+ content: [{
708
+ type: "text",
709
+ text: resultSummary(canonical.status) + (serialized !== void 0 && !(options.operationId === "tinycloud.secrets.get" && canonical.status === "ok") ? `
710
+ ${serialized}` : "")
711
+ }],
658
712
  structuredContent: canonical
659
713
  };
660
714
  }
@@ -702,6 +756,9 @@ var packageRequire = createRequire(
702
756
  var generatedCatalog = packageRequire(
703
757
  "@tinycloud/operations/operations.json"
704
758
  );
759
+ var stableErrorCodes = Object.fromEntries(
760
+ generatedCatalog.stableErrors.map((code) => [code, true])
761
+ );
705
762
  var ALL_SURFACES = ["local", "hosted"];
706
763
  function bindingSurfaces(binding) {
707
764
  return binding.surfaces ?? ALL_SURFACES;
@@ -865,6 +922,36 @@ var TOOL_BINDINGS = [
865
922
  // Hosted has no persistent reauthorization path for a revoked session
866
923
  // yet (TC-896), so status is local/stdio only.
867
924
  surfaces: ["local"]
925
+ },
926
+ {
927
+ name: "tinycloud_apps_context",
928
+ operationId: "tinycloud.apps.context",
929
+ operationVersion: 1,
930
+ readOnlyHint: true,
931
+ idempotentHint: true,
932
+ destructiveHint: false,
933
+ openWorldHint: true,
934
+ surfaces: ["hosted"]
935
+ },
936
+ {
937
+ name: "tinycloud_apps_read",
938
+ operationId: "tinycloud.apps.read",
939
+ operationVersion: 1,
940
+ readOnlyHint: true,
941
+ idempotentHint: true,
942
+ destructiveHint: false,
943
+ openWorldHint: true,
944
+ surfaces: ["hosted"]
945
+ },
946
+ {
947
+ name: "tinycloud_apps_write",
948
+ operationId: "tinycloud.apps.write",
949
+ operationVersion: 1,
950
+ readOnlyHint: false,
951
+ idempotentHint: false,
952
+ destructiveHint: true,
953
+ openWorldHint: true,
954
+ surfaces: ["hosted"]
868
955
  }
869
956
  ];
870
957
  function createJsonSchemaValidator() {
@@ -909,23 +996,50 @@ function registerTinyCloudTools(server, selection, validator) {
909
996
  annotations
910
997
  },
911
998
  async (input) => {
912
- const result = await invokeOperation2(
913
- binding.operationId,
914
- binding.operationVersion,
915
- invocationTargetForMcp(selection),
916
- input
917
- );
918
- const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
919
- return toMcpToolResult(projected, TOOL_BINDINGS);
999
+ const startedAt = performance.now();
1000
+ const metrics = { replayedGrants: 0 };
1001
+ let code = "INTERNAL_ERROR";
1002
+ try {
1003
+ const result = await invokeOperation2(
1004
+ binding.operationId,
1005
+ binding.operationVersion,
1006
+ invocationTargetForMcp(selection),
1007
+ input,
1008
+ metrics
1009
+ );
1010
+ const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
1011
+ const response = toMcpToolResult(projected, TOOL_BINDINGS, {
1012
+ surface: selection.surface,
1013
+ readOnly: binding.readOnlyHint,
1014
+ operationId: binding.operationId
1015
+ });
1016
+ code = outcomeCode(response.structuredContent);
1017
+ return response;
1018
+ } finally {
1019
+ if (selection.surface === "hosted") {
1020
+ logToolCall(binding.name, code, startedAt, metrics.replayedGrants);
1021
+ }
1022
+ }
920
1023
  }
921
1024
  );
922
1025
  }
923
1026
  }
1027
+ function outcomeCode(result) {
1028
+ if (result.status === "error") {
1029
+ const error = result.error;
1030
+ return typeof error?.code === "string" && Object.hasOwn(stableErrorCodes, error.code) ? error.code : "INTERNAL_ERROR";
1031
+ }
1032
+ if (result.status !== "ok") return result.status;
1033
+ const output = result.output;
1034
+ if (output?.outcome === "COPY_PARTIAL") return "COPY_PARTIAL";
1035
+ if (Array.isArray(output?.ops) && output.ops.some((op) => op.outcome !== "ok")) return "PARTIAL";
1036
+ return "ok";
1037
+ }
924
1038
 
925
1039
  // package.json
926
1040
  var package_default = {
927
1041
  name: "@tinycloud/mcp",
928
- version: "0.5.5-beta.0",
1042
+ version: "0.6.0-beta.1",
929
1043
  description: "TinyCloud delegated operations MCP server",
930
1044
  keywords: [
931
1045
  "tinycloud",
@@ -998,7 +1112,7 @@ var package_default = {
998
1112
  "@modelcontextprotocol/server": "2.0.0-beta.4",
999
1113
  "@tinycloud/node-sdk": "3.2.5-beta.0",
1000
1114
  "@tinycloud/node-sdk-wasm": "1.7.7",
1001
- "@tinycloud/operations": "0.5.5-beta.0",
1115
+ "@tinycloud/operations": "0.6.0-beta.1",
1002
1116
  jose: "6.1.3"
1003
1117
  },
1004
1118
  devDependencies: {
@@ -1042,13 +1156,13 @@ function createHostedMcpApp(config) {
1042
1156
  const handler = createMcpHandler(async ({ authInfo: authInfo2 }) => {
1043
1157
  const identity = oauthIdentity(authInfo2);
1044
1158
  const { subject, ownerDids } = identity;
1045
- const stateRoot = store.tenantStateRoot(subject);
1159
+ const stateRoot = await store.initializeTenant(subject);
1046
1160
  const validator = createJsonSchemaValidator();
1047
1161
  const server = new McpServer2(
1048
1162
  { name: MCP_SERVER_NAME, version: MCP_VERSION },
1049
1163
  {
1050
1164
  jsonSchemaValidator: validator,
1051
- instructions: "Call tinycloud_connect first. If an operation returns authority_required, open approval.url, then retry that operation unchanged."
1165
+ 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."
1052
1166
  }
1053
1167
  );
1054
1168
  registerTinyCloudTools(server, {
@@ -1160,14 +1274,21 @@ function registerConnectTool(server, validator, store, identity) {
1160
1274
  }
1161
1275
  },
1162
1276
  async () => {
1163
- const status = await store.connectStatus(identity.subject, identity.ownerDids);
1164
- return {
1165
- content: [{
1166
- type: "text",
1167
- text: status.connected ? "TinyCloud is connected." : "TinyCloud approval is required; open approvalUrl, then call this tool again."
1168
- }],
1169
- structuredContent: status
1170
- };
1277
+ const startedAt = performance.now();
1278
+ let code = "INTERNAL_ERROR";
1279
+ try {
1280
+ const status = await store.connectStatus(identity.subject, identity.ownerDids);
1281
+ code = status.connected ? "ok" : "authority_required";
1282
+ return {
1283
+ content: [{
1284
+ type: "text",
1285
+ text: status.connected ? "TinyCloud is connected." : `TinyCloud approval is required. Open ${status.approvalUrl}, then call this tool again. This link expires at ${status.expiresAt}.`
1286
+ }],
1287
+ structuredContent: status
1288
+ };
1289
+ } finally {
1290
+ logToolCall("tinycloud_connect", code, startedAt, 0);
1291
+ }
1171
1292
  }
1172
1293
  );
1173
1294
  }