@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.js CHANGED
@@ -14,15 +14,37 @@ import { addFormats, Ajv, AjvJsonSchemaValidator } from "@modelcontextprotocol/s
14
14
  import { invokeOperation } from "@tinycloud/operations";
15
15
 
16
16
  // src/results.ts
17
- function toMcpToolResult(result, bindings = []) {
18
- const canonical = isCanonicalOperationResult(result) ? withRecoveryTool(result, bindings) : {
17
+ function toMcpToolResult(result, bindings = [], options = {}) {
18
+ let canonical = isCanonicalOperationResult(result) ? withRecoveryTool(result, bindings) : {
19
19
  status: "error",
20
20
  operation: {},
21
21
  context: {},
22
22
  error: { code: "INTERNAL_ERROR" }
23
23
  };
24
+ let serialized;
25
+ if (options.surface === "hosted") {
26
+ serialized = JSON.stringify(canonical);
27
+ if (options.readOnly === true && serialized.length > 1e5) {
28
+ const sql = options.operationId?.startsWith("tinycloud.sql.") || options.operationId === "tinycloud.apps.query";
29
+ canonical = {
30
+ status: "error",
31
+ operation: canonical.operation,
32
+ context: canonical.context,
33
+ error: {
34
+ code: sql ? "SQL_RESULT_LIMIT_EXCEEDED" : "KV_RESPONSE_TOO_LARGE",
35
+ 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.",
36
+ retryable: false
37
+ }
38
+ };
39
+ serialized = JSON.stringify(canonical);
40
+ }
41
+ }
24
42
  return {
25
- content: [{ type: "text", text: resultSummary(canonical.status) }],
43
+ content: [{
44
+ type: "text",
45
+ text: resultSummary(canonical.status) + (serialized !== void 0 && !(options.operationId === "tinycloud.secrets.get" && canonical.status === "ok") ? `
46
+ ${serialized}` : "")
47
+ }],
26
48
  structuredContent: canonical
27
49
  };
28
50
  }
@@ -59,6 +81,17 @@ function resultSummary(status) {
59
81
  }
60
82
  }
61
83
 
84
+ // src/logging.ts
85
+ function logToolCall(tool, code, startedAt, replayedGrants) {
86
+ process.stderr.write(`${JSON.stringify({
87
+ tool,
88
+ code,
89
+ durationMs: Math.max(0, Math.round(performance.now() - startedAt)),
90
+ replayedGrants
91
+ })}
92
+ `);
93
+ }
94
+
62
95
  // src/tools.ts
63
96
  var TOOL_NAMES = [
64
97
  "tinycloud_status",
@@ -77,7 +110,11 @@ var TOOL_NAMES = [
77
110
  "tinycloud_sql_query",
78
111
  "tinycloud_sql_execute",
79
112
  "tinycloud_secrets_get",
80
- "tinycloud_secrets_status"
113
+ "tinycloud_secrets_status",
114
+ "tinycloud_apps_context",
115
+ "tinycloud_apps_read",
116
+ "tinycloud_apps_write",
117
+ "tinycloud_apps_query"
81
118
  ];
82
119
  function invocationTargetForMcp(selection) {
83
120
  return {
@@ -92,6 +129,9 @@ var packageRequire = createRequire(
92
129
  var generatedCatalog = packageRequire(
93
130
  "@tinycloud/operations/operations.json"
94
131
  );
132
+ var stableErrorCodes = Object.fromEntries(
133
+ generatedCatalog.stableErrors.map((code) => [code, true])
134
+ );
95
135
  var ALL_SURFACES = ["local", "hosted"];
96
136
  function bindingSurfaces(binding) {
97
137
  return binding.surfaces ?? ALL_SURFACES;
@@ -255,6 +295,46 @@ var TOOL_BINDINGS = [
255
295
  // Hosted has no persistent reauthorization path for a revoked session
256
296
  // yet (TC-896), so status is local/stdio only.
257
297
  surfaces: ["local"]
298
+ },
299
+ {
300
+ name: "tinycloud_apps_context",
301
+ operationId: "tinycloud.apps.context",
302
+ operationVersion: 1,
303
+ readOnlyHint: true,
304
+ idempotentHint: true,
305
+ destructiveHint: false,
306
+ openWorldHint: true,
307
+ surfaces: ["hosted"]
308
+ },
309
+ {
310
+ name: "tinycloud_apps_read",
311
+ operationId: "tinycloud.apps.read",
312
+ operationVersion: 1,
313
+ readOnlyHint: true,
314
+ idempotentHint: true,
315
+ destructiveHint: false,
316
+ openWorldHint: true,
317
+ surfaces: ["hosted"]
318
+ },
319
+ {
320
+ name: "tinycloud_apps_write",
321
+ operationId: "tinycloud.apps.write",
322
+ operationVersion: 1,
323
+ readOnlyHint: false,
324
+ idempotentHint: false,
325
+ destructiveHint: true,
326
+ openWorldHint: true,
327
+ surfaces: ["hosted"]
328
+ },
329
+ {
330
+ name: "tinycloud_apps_query",
331
+ operationId: "tinycloud.apps.query",
332
+ operationVersion: 1,
333
+ readOnlyHint: true,
334
+ idempotentHint: true,
335
+ destructiveHint: false,
336
+ openWorldHint: true,
337
+ surfaces: ["hosted"]
258
338
  }
259
339
  ];
260
340
  function createJsonSchemaValidator() {
@@ -299,23 +379,50 @@ function registerTinyCloudTools(server, selection, validator) {
299
379
  annotations
300
380
  },
301
381
  async (input) => {
302
- const result = await invokeOperation(
303
- binding.operationId,
304
- binding.operationVersion,
305
- invocationTargetForMcp(selection),
306
- input
307
- );
308
- const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
309
- return toMcpToolResult(projected, TOOL_BINDINGS);
382
+ const startedAt = performance.now();
383
+ const metrics = { replayedGrants: 0 };
384
+ let code = "INTERNAL_ERROR";
385
+ try {
386
+ const result = await invokeOperation(
387
+ binding.operationId,
388
+ binding.operationVersion,
389
+ invocationTargetForMcp(selection),
390
+ input,
391
+ metrics
392
+ );
393
+ const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
394
+ const response = toMcpToolResult(projected, TOOL_BINDINGS, {
395
+ surface: selection.surface,
396
+ readOnly: binding.readOnlyHint,
397
+ operationId: binding.operationId
398
+ });
399
+ code = outcomeCode(response.structuredContent);
400
+ return response;
401
+ } finally {
402
+ if (selection.surface === "hosted") {
403
+ logToolCall(binding.name, code, startedAt, metrics.replayedGrants);
404
+ }
405
+ }
310
406
  }
311
407
  );
312
408
  }
313
409
  }
410
+ function outcomeCode(result) {
411
+ if (result.status === "error") {
412
+ const error = result.error;
413
+ return typeof error?.code === "string" && Object.hasOwn(stableErrorCodes, error.code) ? error.code : "INTERNAL_ERROR";
414
+ }
415
+ if (result.status !== "ok") return result.status;
416
+ const output = result.output;
417
+ if (output?.outcome === "COPY_PARTIAL") return "COPY_PARTIAL";
418
+ if (Array.isArray(output?.ops) && output.ops.some((op) => op.outcome !== "ok")) return "PARTIAL";
419
+ return "ok";
420
+ }
314
421
 
315
422
  // package.json
316
423
  var package_default = {
317
424
  name: "@tinycloud/mcp",
318
- version: "0.5.5-beta.0",
425
+ version: "0.6.0-beta.2",
319
426
  description: "TinyCloud delegated operations MCP server",
320
427
  keywords: [
321
428
  "tinycloud",
@@ -388,7 +495,7 @@ var package_default = {
388
495
  "@modelcontextprotocol/server": "2.0.0-beta.4",
389
496
  "@tinycloud/node-sdk": "3.2.5-beta.0",
390
497
  "@tinycloud/node-sdk-wasm": "1.7.7",
391
- "@tinycloud/operations": "0.5.5-beta.0",
498
+ "@tinycloud/operations": "0.6.0-beta.2",
392
499
  jose: "6.1.3"
393
500
  },
394
501
  devDependencies: {
@@ -633,6 +740,7 @@ import {
633
740
  } from "@tinycloud/node-sdk";
634
741
  import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk-wasm";
635
742
  import { invokeOperation as invokeOperation2 } from "@tinycloud/operations";
743
+ import { tcAppsApprovalMessage, tcAppsApprovalReason } from "@tinycloud/operations/tc-apps";
636
744
  var PROFILE = "agent";
637
745
  var MAX_CALLBACK_BYTES = 256 * 1024;
638
746
  var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
@@ -685,6 +793,10 @@ var RemoteTenantStore = class {
685
793
  tenantStateRoot(subject) {
686
794
  return join2(this.#config.stateDir, "tenants", this.#tenantId(subject));
687
795
  }
796
+ /** Prepare only this authenticated account's profile before its first operation. */
797
+ async initializeTenant(subject) {
798
+ return (await this.#ensureTenant(subject)).stateRoot;
799
+ }
688
800
  async connectStatus(subject, ownerDids = []) {
689
801
  const tenant = await this.#ensureTenant(subject);
690
802
  if (await this.#hasValidSession(tenant, ownerDids)) {
@@ -699,7 +811,23 @@ var RemoteTenantStore = class {
699
811
  };
700
812
  }
701
813
  async decorateAuthorityResult(subject, ownerDids, result) {
702
- if (!isRecord(result) || result.status !== "authority_required") return result;
814
+ if (!isRecord(result)) return result;
815
+ if (result.status === "error" && isRecord(result.error) && (result.error.code === "SESSION_NOT_FOUND" || result.error.code === "AUTH_REQUIRED")) {
816
+ await this.#ensureTenant(subject);
817
+ const pending2 = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
818
+ return {
819
+ ...result,
820
+ error: {
821
+ ...result.error,
822
+ details: {
823
+ ...isRecord(result.error.details) ? result.error.details : {},
824
+ approvalUrl: this.#connectUrl(pending2),
825
+ expiresAt: pending2.expiresAt
826
+ }
827
+ }
828
+ };
829
+ }
830
+ if (result.status !== "authority_required") return result;
703
831
  const request = isRecord(result.request) ? result.request : void 0;
704
832
  const requestId = typeof request?.requestId === "string" ? request.requestId : void 0;
705
833
  const requested = Array.isArray(request?.requested) ? request.requested.filter(isPermissionEntry) : [];
@@ -712,7 +840,7 @@ var RemoteTenantStore = class {
712
840
  kind: "openkey",
713
841
  requestId,
714
842
  url: this.#connectUrl(pending),
715
- fallback: "Open the approval URL, then retry the same MCP tool."
843
+ fallback: tcAppsApprovalMessage(requested) ?? "Open the approval URL, then retry the same MCP tool."
716
844
  }
717
845
  };
718
846
  }
@@ -729,7 +857,7 @@ var RemoteTenantStore = class {
729
857
  permissions: Buffer.from(JSON.stringify({
730
858
  permissions: pending.permissions,
731
859
  reason: [
732
- pending.kind === "bootstrap" ? "Connect this TinyCloud account to the hosted MCP delegate." : "Approve the exact capabilities requested by the TinyCloud MCP operation.",
860
+ 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.",
733
861
  // Owner DIDs come from the verified OpenKey access token of the MCP
734
862
  // client that created this link, never from the link itself.
735
863
  ...pending.ownerDids.length === 0 ? [] : [`Requested by TinyCloud account ${pending.ownerDids.join(", ")}; approve only if that is you.`]
@@ -1167,13 +1295,13 @@ function createHostedMcpApp(config) {
1167
1295
  const handler = createMcpHandler(async ({ authInfo: authInfo2 }) => {
1168
1296
  const identity = oauthIdentity(authInfo2);
1169
1297
  const { subject, ownerDids } = identity;
1170
- const stateRoot = store.tenantStateRoot(subject);
1298
+ const stateRoot = await store.initializeTenant(subject);
1171
1299
  const validator = createJsonSchemaValidator();
1172
1300
  const server = new McpServer2(
1173
1301
  { name: MCP_SERVER_NAME, version: MCP_VERSION },
1174
1302
  {
1175
1303
  jsonSchemaValidator: validator,
1176
- instructions: "Call tinycloud_connect first. If an operation returns authority_required, open approval.url, then retry that operation unchanged."
1304
+ 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."
1177
1305
  }
1178
1306
  );
1179
1307
  registerTinyCloudTools(server, {
@@ -1285,14 +1413,21 @@ function registerConnectTool(server, validator, store, identity) {
1285
1413
  }
1286
1414
  },
1287
1415
  async () => {
1288
- const status = await store.connectStatus(identity.subject, identity.ownerDids);
1289
- return {
1290
- content: [{
1291
- type: "text",
1292
- text: status.connected ? "TinyCloud is connected." : "TinyCloud approval is required; open approvalUrl, then call this tool again."
1293
- }],
1294
- structuredContent: status
1295
- };
1416
+ const startedAt = performance.now();
1417
+ let code = "INTERNAL_ERROR";
1418
+ try {
1419
+ const status = await store.connectStatus(identity.subject, identity.ownerDids);
1420
+ code = status.connected ? "ok" : "authority_required";
1421
+ return {
1422
+ content: [{
1423
+ type: "text",
1424
+ text: status.connected ? "TinyCloud is connected." : `TinyCloud approval is required. Open ${status.approvalUrl}, then call this tool again. This link expires at ${status.expiresAt}.`
1425
+ }],
1426
+ structuredContent: status
1427
+ };
1428
+ } finally {
1429
+ logToolCall("tinycloud_connect", code, startedAt, 0);
1430
+ }
1296
1431
  }
1297
1432
  );
1298
1433
  }