@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/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,11 @@ 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",
154
+ "tinycloud_apps_query"
118
155
  ];
119
156
  function invocationTargetForMcp(selection) {
120
157
  return {
@@ -129,6 +166,9 @@ var packageRequire = (0, import_node_module.createRequire)(
129
166
  var generatedCatalog = packageRequire(
130
167
  "@tinycloud/operations/operations.json"
131
168
  );
169
+ var stableErrorCodes = Object.fromEntries(
170
+ generatedCatalog.stableErrors.map((code) => [code, true])
171
+ );
132
172
  var ALL_SURFACES = ["local", "hosted"];
133
173
  function bindingSurfaces(binding) {
134
174
  return binding.surfaces ?? ALL_SURFACES;
@@ -292,6 +332,46 @@ var TOOL_BINDINGS = [
292
332
  // Hosted has no persistent reauthorization path for a revoked session
293
333
  // yet (TC-896), so status is local/stdio only.
294
334
  surfaces: ["local"]
335
+ },
336
+ {
337
+ name: "tinycloud_apps_context",
338
+ operationId: "tinycloud.apps.context",
339
+ operationVersion: 1,
340
+ readOnlyHint: true,
341
+ idempotentHint: true,
342
+ destructiveHint: false,
343
+ openWorldHint: true,
344
+ surfaces: ["hosted"]
345
+ },
346
+ {
347
+ name: "tinycloud_apps_read",
348
+ operationId: "tinycloud.apps.read",
349
+ operationVersion: 1,
350
+ readOnlyHint: true,
351
+ idempotentHint: true,
352
+ destructiveHint: false,
353
+ openWorldHint: true,
354
+ surfaces: ["hosted"]
355
+ },
356
+ {
357
+ name: "tinycloud_apps_write",
358
+ operationId: "tinycloud.apps.write",
359
+ operationVersion: 1,
360
+ readOnlyHint: false,
361
+ idempotentHint: false,
362
+ destructiveHint: true,
363
+ openWorldHint: true,
364
+ surfaces: ["hosted"]
365
+ },
366
+ {
367
+ name: "tinycloud_apps_query",
368
+ operationId: "tinycloud.apps.query",
369
+ operationVersion: 1,
370
+ readOnlyHint: true,
371
+ idempotentHint: true,
372
+ destructiveHint: false,
373
+ openWorldHint: true,
374
+ surfaces: ["hosted"]
295
375
  }
296
376
  ];
297
377
  function createJsonSchemaValidator() {
@@ -336,23 +416,50 @@ function registerTinyCloudTools(server, selection, validator) {
336
416
  annotations
337
417
  },
338
418
  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);
419
+ const startedAt = performance.now();
420
+ const metrics = { replayedGrants: 0 };
421
+ let code = "INTERNAL_ERROR";
422
+ try {
423
+ const result = await (0, import_operations.invokeOperation)(
424
+ binding.operationId,
425
+ binding.operationVersion,
426
+ invocationTargetForMcp(selection),
427
+ input,
428
+ metrics
429
+ );
430
+ const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
431
+ const response = toMcpToolResult(projected, TOOL_BINDINGS, {
432
+ surface: selection.surface,
433
+ readOnly: binding.readOnlyHint,
434
+ operationId: binding.operationId
435
+ });
436
+ code = outcomeCode(response.structuredContent);
437
+ return response;
438
+ } finally {
439
+ if (selection.surface === "hosted") {
440
+ logToolCall(binding.name, code, startedAt, metrics.replayedGrants);
441
+ }
442
+ }
347
443
  }
348
444
  );
349
445
  }
350
446
  }
447
+ function outcomeCode(result) {
448
+ if (result.status === "error") {
449
+ const error = result.error;
450
+ return typeof error?.code === "string" && Object.hasOwn(stableErrorCodes, error.code) ? error.code : "INTERNAL_ERROR";
451
+ }
452
+ if (result.status !== "ok") return result.status;
453
+ const output = result.output;
454
+ if (output?.outcome === "COPY_PARTIAL") return "COPY_PARTIAL";
455
+ if (Array.isArray(output?.ops) && output.ops.some((op) => op.outcome !== "ok")) return "PARTIAL";
456
+ return "ok";
457
+ }
351
458
 
352
459
  // package.json
353
460
  var package_default = {
354
461
  name: "@tinycloud/mcp",
355
- version: "0.5.5-beta.0",
462
+ version: "0.6.0-beta.2",
356
463
  description: "TinyCloud delegated operations MCP server",
357
464
  keywords: [
358
465
  "tinycloud",
@@ -425,7 +532,7 @@ var package_default = {
425
532
  "@modelcontextprotocol/server": "2.0.0-beta.4",
426
533
  "@tinycloud/node-sdk": "3.2.5-beta.0",
427
534
  "@tinycloud/node-sdk-wasm": "1.7.7",
428
- "@tinycloud/operations": "0.5.5-beta.0",
535
+ "@tinycloud/operations": "0.6.0-beta.2",
429
536
  jose: "6.1.3"
430
537
  },
431
538
  devDependencies: {
@@ -651,6 +758,7 @@ var import_node_path3 = require("path");
651
758
  var import_node_sdk2 = require("@tinycloud/node-sdk");
652
759
  var import_node_sdk_wasm = require("@tinycloud/node-sdk-wasm");
653
760
  var import_operations2 = require("@tinycloud/operations");
761
+ var import_tc_apps = require("@tinycloud/operations/tc-apps");
654
762
  var PROFILE = "agent";
655
763
  var MAX_CALLBACK_BYTES = 256 * 1024;
656
764
  var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
@@ -703,6 +811,10 @@ var RemoteTenantStore = class {
703
811
  tenantStateRoot(subject) {
704
812
  return (0, import_node_path3.join)(this.#config.stateDir, "tenants", this.#tenantId(subject));
705
813
  }
814
+ /** Prepare only this authenticated account's profile before its first operation. */
815
+ async initializeTenant(subject) {
816
+ return (await this.#ensureTenant(subject)).stateRoot;
817
+ }
706
818
  async connectStatus(subject, ownerDids = []) {
707
819
  const tenant = await this.#ensureTenant(subject);
708
820
  if (await this.#hasValidSession(tenant, ownerDids)) {
@@ -717,7 +829,23 @@ var RemoteTenantStore = class {
717
829
  };
718
830
  }
719
831
  async decorateAuthorityResult(subject, ownerDids, result) {
720
- if (!isRecord(result) || result.status !== "authority_required") return result;
832
+ if (!isRecord(result)) return result;
833
+ if (result.status === "error" && isRecord(result.error) && (result.error.code === "SESSION_NOT_FOUND" || result.error.code === "AUTH_REQUIRED")) {
834
+ await this.#ensureTenant(subject);
835
+ const pending2 = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
836
+ return {
837
+ ...result,
838
+ error: {
839
+ ...result.error,
840
+ details: {
841
+ ...isRecord(result.error.details) ? result.error.details : {},
842
+ approvalUrl: this.#connectUrl(pending2),
843
+ expiresAt: pending2.expiresAt
844
+ }
845
+ }
846
+ };
847
+ }
848
+ if (result.status !== "authority_required") return result;
721
849
  const request = isRecord(result.request) ? result.request : void 0;
722
850
  const requestId = typeof request?.requestId === "string" ? request.requestId : void 0;
723
851
  const requested = Array.isArray(request?.requested) ? request.requested.filter(isPermissionEntry) : [];
@@ -730,7 +858,7 @@ var RemoteTenantStore = class {
730
858
  kind: "openkey",
731
859
  requestId,
732
860
  url: this.#connectUrl(pending),
733
- fallback: "Open the approval URL, then retry the same MCP tool."
861
+ fallback: (0, import_tc_apps.tcAppsApprovalMessage)(requested) ?? "Open the approval URL, then retry the same MCP tool."
734
862
  }
735
863
  };
736
864
  }
@@ -747,7 +875,7 @@ var RemoteTenantStore = class {
747
875
  permissions: Buffer.from(JSON.stringify({
748
876
  permissions: pending.permissions,
749
877
  reason: [
750
- pending.kind === "bootstrap" ? "Connect this TinyCloud account to the hosted MCP delegate." : "Approve the exact capabilities requested by the TinyCloud MCP operation.",
878
+ 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
879
  // Owner DIDs come from the verified OpenKey access token of the MCP
752
880
  // client that created this link, never from the link itself.
753
881
  ...pending.ownerDids.length === 0 ? [] : [`Requested by TinyCloud account ${pending.ownerDids.join(", ")}; approve only if that is you.`]
@@ -1185,13 +1313,13 @@ function createHostedMcpApp(config) {
1185
1313
  const handler = (0, import_server5.createMcpHandler)(async ({ authInfo: authInfo2 }) => {
1186
1314
  const identity = oauthIdentity(authInfo2);
1187
1315
  const { subject, ownerDids } = identity;
1188
- const stateRoot = store.tenantStateRoot(subject);
1316
+ const stateRoot = await store.initializeTenant(subject);
1189
1317
  const validator = createJsonSchemaValidator();
1190
1318
  const server = new import_server5.McpServer(
1191
1319
  { name: MCP_SERVER_NAME, version: MCP_VERSION },
1192
1320
  {
1193
1321
  jsonSchemaValidator: validator,
1194
- instructions: "Call tinycloud_connect first. If an operation returns authority_required, open approval.url, then retry that operation unchanged."
1322
+ 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
1323
  }
1196
1324
  );
1197
1325
  registerTinyCloudTools(server, {
@@ -1303,14 +1431,21 @@ function registerConnectTool(server, validator, store, identity) {
1303
1431
  }
1304
1432
  },
1305
1433
  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
- };
1434
+ const startedAt = performance.now();
1435
+ let code = "INTERNAL_ERROR";
1436
+ try {
1437
+ const status = await store.connectStatus(identity.subject, identity.ownerDids);
1438
+ code = status.connected ? "ok" : "authority_required";
1439
+ return {
1440
+ content: [{
1441
+ type: "text",
1442
+ text: status.connected ? "TinyCloud is connected." : `TinyCloud approval is required. Open ${status.approvalUrl}, then call this tool again. This link expires at ${status.expiresAt}.`
1443
+ }],
1444
+ structuredContent: status
1445
+ };
1446
+ } finally {
1447
+ logToolCall("tinycloud_connect", code, startedAt, 0);
1448
+ }
1314
1449
  }
1315
1450
  );
1316
1451
  }