@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-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,46 @@ 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"]
955
+ },
956
+ {
957
+ name: "tinycloud_apps_query",
958
+ operationId: "tinycloud.apps.query",
959
+ operationVersion: 1,
960
+ readOnlyHint: true,
961
+ idempotentHint: true,
962
+ destructiveHint: false,
963
+ openWorldHint: true,
964
+ surfaces: ["hosted"]
868
965
  }
869
966
  ];
870
967
  function createJsonSchemaValidator() {
@@ -909,23 +1006,50 @@ function registerTinyCloudTools(server, selection, validator) {
909
1006
  annotations
910
1007
  },
911
1008
  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);
1009
+ const startedAt = performance.now();
1010
+ const metrics = { replayedGrants: 0 };
1011
+ let code = "INTERNAL_ERROR";
1012
+ try {
1013
+ const result = await invokeOperation2(
1014
+ binding.operationId,
1015
+ binding.operationVersion,
1016
+ invocationTargetForMcp(selection),
1017
+ input,
1018
+ metrics
1019
+ );
1020
+ const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
1021
+ const response = toMcpToolResult(projected, TOOL_BINDINGS, {
1022
+ surface: selection.surface,
1023
+ readOnly: binding.readOnlyHint,
1024
+ operationId: binding.operationId
1025
+ });
1026
+ code = outcomeCode(response.structuredContent);
1027
+ return response;
1028
+ } finally {
1029
+ if (selection.surface === "hosted") {
1030
+ logToolCall(binding.name, code, startedAt, metrics.replayedGrants);
1031
+ }
1032
+ }
920
1033
  }
921
1034
  );
922
1035
  }
923
1036
  }
1037
+ function outcomeCode(result) {
1038
+ if (result.status === "error") {
1039
+ const error = result.error;
1040
+ return typeof error?.code === "string" && Object.hasOwn(stableErrorCodes, error.code) ? error.code : "INTERNAL_ERROR";
1041
+ }
1042
+ if (result.status !== "ok") return result.status;
1043
+ const output = result.output;
1044
+ if (output?.outcome === "COPY_PARTIAL") return "COPY_PARTIAL";
1045
+ if (Array.isArray(output?.ops) && output.ops.some((op) => op.outcome !== "ok")) return "PARTIAL";
1046
+ return "ok";
1047
+ }
924
1048
 
925
1049
  // package.json
926
1050
  var package_default = {
927
1051
  name: "@tinycloud/mcp",
928
- version: "0.5.5-beta.0",
1052
+ version: "0.6.0-beta.2",
929
1053
  description: "TinyCloud delegated operations MCP server",
930
1054
  keywords: [
931
1055
  "tinycloud",
@@ -998,7 +1122,7 @@ var package_default = {
998
1122
  "@modelcontextprotocol/server": "2.0.0-beta.4",
999
1123
  "@tinycloud/node-sdk": "3.2.5-beta.0",
1000
1124
  "@tinycloud/node-sdk-wasm": "1.7.7",
1001
- "@tinycloud/operations": "0.5.5-beta.0",
1125
+ "@tinycloud/operations": "0.6.0-beta.2",
1002
1126
  jose: "6.1.3"
1003
1127
  },
1004
1128
  devDependencies: {
@@ -1042,13 +1166,13 @@ function createHostedMcpApp(config) {
1042
1166
  const handler = createMcpHandler(async ({ authInfo: authInfo2 }) => {
1043
1167
  const identity = oauthIdentity(authInfo2);
1044
1168
  const { subject, ownerDids } = identity;
1045
- const stateRoot = store.tenantStateRoot(subject);
1169
+ const stateRoot = await store.initializeTenant(subject);
1046
1170
  const validator = createJsonSchemaValidator();
1047
1171
  const server = new McpServer2(
1048
1172
  { name: MCP_SERVER_NAME, version: MCP_VERSION },
1049
1173
  {
1050
1174
  jsonSchemaValidator: validator,
1051
- instructions: "Call tinycloud_connect first. If an operation returns authority_required, open approval.url, then retry that operation unchanged."
1175
+ 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
1176
  }
1053
1177
  );
1054
1178
  registerTinyCloudTools(server, {
@@ -1160,14 +1284,21 @@ function registerConnectTool(server, validator, store, identity) {
1160
1284
  }
1161
1285
  },
1162
1286
  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
- };
1287
+ const startedAt = performance.now();
1288
+ let code = "INTERNAL_ERROR";
1289
+ try {
1290
+ const status = await store.connectStatus(identity.subject, identity.ownerDids);
1291
+ code = status.connected ? "ok" : "authority_required";
1292
+ return {
1293
+ content: [{
1294
+ type: "text",
1295
+ text: status.connected ? "TinyCloud is connected." : `TinyCloud approval is required. Open ${status.approvalUrl}, then call this tool again. This link expires at ${status.expiresAt}.`
1296
+ }],
1297
+ structuredContent: status
1298
+ };
1299
+ } finally {
1300
+ logToolCall("tinycloud_connect", code, startedAt, 0);
1301
+ }
1171
1302
  }
1172
1303
  );
1173
1304
  }