@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/index.cjs CHANGED
@@ -50,15 +50,37 @@ var import_ajv = require("@modelcontextprotocol/server/validators/ajv");
50
50
  var import_operations = require("@tinycloud/operations");
51
51
 
52
52
  // src/results.ts
53
- function toMcpToolResult(result, bindings = []) {
54
- const canonical = isCanonicalOperationResult(result) ? withRecoveryTool(result, bindings) : {
53
+ function toMcpToolResult(result, bindings = [], options = {}) {
54
+ let canonical = isCanonicalOperationResult(result) ? withRecoveryTool(result, bindings) : {
55
55
  status: "error",
56
56
  operation: {},
57
57
  context: {},
58
58
  error: { code: "INTERNAL_ERROR" }
59
59
  };
60
+ let serialized;
61
+ if (options.surface === "hosted") {
62
+ serialized = JSON.stringify(canonical);
63
+ if (options.readOnly === true && serialized.length > 1e5) {
64
+ const sql = options.operationId?.startsWith("tinycloud.sql.") || options.operationId === "tinycloud.apps.query";
65
+ canonical = {
66
+ status: "error",
67
+ operation: canonical.operation,
68
+ context: canonical.context,
69
+ error: {
70
+ code: sql ? "SQL_RESULT_LIMIT_EXCEEDED" : "KV_RESPONSE_TOO_LARGE",
71
+ 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.",
72
+ retryable: false
73
+ }
74
+ };
75
+ serialized = JSON.stringify(canonical);
76
+ }
77
+ }
60
78
  return {
61
- content: [{ type: "text", text: resultSummary(canonical.status) }],
79
+ content: [{
80
+ type: "text",
81
+ text: resultSummary(canonical.status) + (serialized !== void 0 && !(options.operationId === "tinycloud.secrets.get" && canonical.status === "ok") ? `
82
+ ${serialized}` : "")
83
+ }],
62
84
  structuredContent: canonical
63
85
  };
64
86
  }
@@ -95,6 +117,17 @@ function resultSummary(status) {
95
117
  }
96
118
  }
97
119
 
120
+ // src/logging.ts
121
+ function logToolCall(tool, code, startedAt, replayedGrants) {
122
+ process.stderr.write(`${JSON.stringify({
123
+ tool,
124
+ code,
125
+ durationMs: Math.max(0, Math.round(performance.now() - startedAt)),
126
+ replayedGrants
127
+ })}
128
+ `);
129
+ }
130
+
98
131
  // src/tools.ts
99
132
  var import_meta = {};
100
133
  var TOOL_NAMES = [
@@ -114,7 +147,10 @@ var TOOL_NAMES = [
114
147
  "tinycloud_sql_query",
115
148
  "tinycloud_sql_execute",
116
149
  "tinycloud_secrets_get",
117
- "tinycloud_secrets_status"
150
+ "tinycloud_secrets_status",
151
+ "tinycloud_apps_context",
152
+ "tinycloud_apps_read",
153
+ "tinycloud_apps_write"
118
154
  ];
119
155
  function invocationTargetForMcp(selection) {
120
156
  return {
@@ -129,6 +165,9 @@ var packageRequire = (0, import_node_module.createRequire)(
129
165
  var generatedCatalog = packageRequire(
130
166
  "@tinycloud/operations/operations.json"
131
167
  );
168
+ var stableErrorCodes = Object.fromEntries(
169
+ generatedCatalog.stableErrors.map((code) => [code, true])
170
+ );
132
171
  var ALL_SURFACES = ["local", "hosted"];
133
172
  function bindingSurfaces(binding) {
134
173
  return binding.surfaces ?? ALL_SURFACES;
@@ -292,6 +331,36 @@ var TOOL_BINDINGS = [
292
331
  // Hosted has no persistent reauthorization path for a revoked session
293
332
  // yet (TC-896), so status is local/stdio only.
294
333
  surfaces: ["local"]
334
+ },
335
+ {
336
+ name: "tinycloud_apps_context",
337
+ operationId: "tinycloud.apps.context",
338
+ operationVersion: 1,
339
+ readOnlyHint: true,
340
+ idempotentHint: true,
341
+ destructiveHint: false,
342
+ openWorldHint: true,
343
+ surfaces: ["hosted"]
344
+ },
345
+ {
346
+ name: "tinycloud_apps_read",
347
+ operationId: "tinycloud.apps.read",
348
+ operationVersion: 1,
349
+ readOnlyHint: true,
350
+ idempotentHint: true,
351
+ destructiveHint: false,
352
+ openWorldHint: true,
353
+ surfaces: ["hosted"]
354
+ },
355
+ {
356
+ name: "tinycloud_apps_write",
357
+ operationId: "tinycloud.apps.write",
358
+ operationVersion: 1,
359
+ readOnlyHint: false,
360
+ idempotentHint: false,
361
+ destructiveHint: true,
362
+ openWorldHint: true,
363
+ surfaces: ["hosted"]
295
364
  }
296
365
  ];
297
366
  function createJsonSchemaValidator() {
@@ -336,23 +405,50 @@ function registerTinyCloudTools(server, selection, validator) {
336
405
  annotations
337
406
  },
338
407
  async (input) => {
339
- const result = await (0, import_operations.invokeOperation)(
340
- binding.operationId,
341
- binding.operationVersion,
342
- invocationTargetForMcp(selection),
343
- input
344
- );
345
- const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
346
- return toMcpToolResult(projected, TOOL_BINDINGS);
408
+ const startedAt = performance.now();
409
+ const metrics = { replayedGrants: 0 };
410
+ let code = "INTERNAL_ERROR";
411
+ try {
412
+ const result = await (0, import_operations.invokeOperation)(
413
+ binding.operationId,
414
+ binding.operationVersion,
415
+ invocationTargetForMcp(selection),
416
+ input,
417
+ metrics
418
+ );
419
+ const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
420
+ const response = toMcpToolResult(projected, TOOL_BINDINGS, {
421
+ surface: selection.surface,
422
+ readOnly: binding.readOnlyHint,
423
+ operationId: binding.operationId
424
+ });
425
+ code = outcomeCode(response.structuredContent);
426
+ return response;
427
+ } finally {
428
+ if (selection.surface === "hosted") {
429
+ logToolCall(binding.name, code, startedAt, metrics.replayedGrants);
430
+ }
431
+ }
347
432
  }
348
433
  );
349
434
  }
350
435
  }
436
+ function outcomeCode(result) {
437
+ if (result.status === "error") {
438
+ const error = result.error;
439
+ return typeof error?.code === "string" && Object.hasOwn(stableErrorCodes, error.code) ? error.code : "INTERNAL_ERROR";
440
+ }
441
+ if (result.status !== "ok") return result.status;
442
+ const output = result.output;
443
+ if (output?.outcome === "COPY_PARTIAL") return "COPY_PARTIAL";
444
+ if (Array.isArray(output?.ops) && output.ops.some((op) => op.outcome !== "ok")) return "PARTIAL";
445
+ return "ok";
446
+ }
351
447
 
352
448
  // package.json
353
449
  var package_default = {
354
450
  name: "@tinycloud/mcp",
355
- version: "0.5.5-beta.0",
451
+ version: "0.6.0-beta.1",
356
452
  description: "TinyCloud delegated operations MCP server",
357
453
  keywords: [
358
454
  "tinycloud",
@@ -425,7 +521,7 @@ var package_default = {
425
521
  "@modelcontextprotocol/server": "2.0.0-beta.4",
426
522
  "@tinycloud/node-sdk": "3.2.5-beta.0",
427
523
  "@tinycloud/node-sdk-wasm": "1.7.7",
428
- "@tinycloud/operations": "0.5.5-beta.0",
524
+ "@tinycloud/operations": "0.6.0-beta.1",
429
525
  jose: "6.1.3"
430
526
  },
431
527
  devDependencies: {
@@ -651,6 +747,7 @@ var import_node_path3 = require("path");
651
747
  var import_node_sdk2 = require("@tinycloud/node-sdk");
652
748
  var import_node_sdk_wasm = require("@tinycloud/node-sdk-wasm");
653
749
  var import_operations2 = require("@tinycloud/operations");
750
+ var import_tc_apps = require("@tinycloud/operations/tc-apps");
654
751
  var PROFILE = "agent";
655
752
  var MAX_CALLBACK_BYTES = 256 * 1024;
656
753
  var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
@@ -703,6 +800,10 @@ var RemoteTenantStore = class {
703
800
  tenantStateRoot(subject) {
704
801
  return (0, import_node_path3.join)(this.#config.stateDir, "tenants", this.#tenantId(subject));
705
802
  }
803
+ /** Prepare only this authenticated account's profile before its first operation. */
804
+ async initializeTenant(subject) {
805
+ return (await this.#ensureTenant(subject)).stateRoot;
806
+ }
706
807
  async connectStatus(subject, ownerDids = []) {
707
808
  const tenant = await this.#ensureTenant(subject);
708
809
  if (await this.#hasValidSession(tenant, ownerDids)) {
@@ -717,7 +818,23 @@ var RemoteTenantStore = class {
717
818
  };
718
819
  }
719
820
  async decorateAuthorityResult(subject, ownerDids, result) {
720
- if (!isRecord(result) || result.status !== "authority_required") return result;
821
+ if (!isRecord(result)) return result;
822
+ if (result.status === "error" && isRecord(result.error) && (result.error.code === "SESSION_NOT_FOUND" || result.error.code === "AUTH_REQUIRED")) {
823
+ await this.#ensureTenant(subject);
824
+ const pending2 = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
825
+ return {
826
+ ...result,
827
+ error: {
828
+ ...result.error,
829
+ details: {
830
+ ...isRecord(result.error.details) ? result.error.details : {},
831
+ approvalUrl: this.#connectUrl(pending2),
832
+ expiresAt: pending2.expiresAt
833
+ }
834
+ }
835
+ };
836
+ }
837
+ if (result.status !== "authority_required") return result;
721
838
  const request = isRecord(result.request) ? result.request : void 0;
722
839
  const requestId = typeof request?.requestId === "string" ? request.requestId : void 0;
723
840
  const requested = Array.isArray(request?.requested) ? request.requested.filter(isPermissionEntry) : [];
@@ -730,7 +847,7 @@ var RemoteTenantStore = class {
730
847
  kind: "openkey",
731
848
  requestId,
732
849
  url: this.#connectUrl(pending),
733
- fallback: "Open the approval URL, then retry the same MCP tool."
850
+ fallback: (0, import_tc_apps.tcAppsApprovalMessage)(requested) ?? "Open the approval URL, then retry the same MCP tool."
734
851
  }
735
852
  };
736
853
  }
@@ -747,7 +864,7 @@ var RemoteTenantStore = class {
747
864
  permissions: Buffer.from(JSON.stringify({
748
865
  permissions: pending.permissions,
749
866
  reason: [
750
- pending.kind === "bootstrap" ? "Connect this TinyCloud account to the hosted MCP delegate." : "Approve the exact capabilities requested by the TinyCloud MCP operation.",
867
+ 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.",
751
868
  // Owner DIDs come from the verified OpenKey access token of the MCP
752
869
  // client that created this link, never from the link itself.
753
870
  ...pending.ownerDids.length === 0 ? [] : [`Requested by TinyCloud account ${pending.ownerDids.join(", ")}; approve only if that is you.`]
@@ -1185,13 +1302,13 @@ function createHostedMcpApp(config) {
1185
1302
  const handler = (0, import_server5.createMcpHandler)(async ({ authInfo: authInfo2 }) => {
1186
1303
  const identity = oauthIdentity(authInfo2);
1187
1304
  const { subject, ownerDids } = identity;
1188
- const stateRoot = store.tenantStateRoot(subject);
1305
+ const stateRoot = await store.initializeTenant(subject);
1189
1306
  const validator = createJsonSchemaValidator();
1190
1307
  const server = new import_server5.McpServer(
1191
1308
  { name: MCP_SERVER_NAME, version: MCP_VERSION },
1192
1309
  {
1193
1310
  jsonSchemaValidator: validator,
1194
- instructions: "Call tinycloud_connect first. If an operation returns authority_required, open approval.url, then retry that operation unchanged."
1311
+ 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."
1195
1312
  }
1196
1313
  );
1197
1314
  registerTinyCloudTools(server, {
@@ -1303,14 +1420,21 @@ function registerConnectTool(server, validator, store, identity) {
1303
1420
  }
1304
1421
  },
1305
1422
  async () => {
1306
- const status = await store.connectStatus(identity.subject, identity.ownerDids);
1307
- return {
1308
- content: [{
1309
- type: "text",
1310
- text: status.connected ? "TinyCloud is connected." : "TinyCloud approval is required; open approvalUrl, then call this tool again."
1311
- }],
1312
- structuredContent: status
1313
- };
1423
+ const startedAt = performance.now();
1424
+ let code = "INTERNAL_ERROR";
1425
+ try {
1426
+ const status = await store.connectStatus(identity.subject, identity.ownerDids);
1427
+ code = status.connected ? "ok" : "authority_required";
1428
+ return {
1429
+ content: [{
1430
+ type: "text",
1431
+ text: status.connected ? "TinyCloud is connected." : `TinyCloud approval is required. Open ${status.approvalUrl}, then call this tool again. This link expires at ${status.expiresAt}.`
1432
+ }],
1433
+ structuredContent: status
1434
+ };
1435
+ } finally {
1436
+ logToolCall("tinycloud_connect", code, startedAt, 0);
1437
+ }
1314
1438
  }
1315
1439
  );
1316
1440
  }