@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.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,10 @@ 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"
81
117
  ];
82
118
  function invocationTargetForMcp(selection) {
83
119
  return {
@@ -92,6 +128,9 @@ var packageRequire = createRequire(
92
128
  var generatedCatalog = packageRequire(
93
129
  "@tinycloud/operations/operations.json"
94
130
  );
131
+ var stableErrorCodes = Object.fromEntries(
132
+ generatedCatalog.stableErrors.map((code) => [code, true])
133
+ );
95
134
  var ALL_SURFACES = ["local", "hosted"];
96
135
  function bindingSurfaces(binding) {
97
136
  return binding.surfaces ?? ALL_SURFACES;
@@ -255,6 +294,36 @@ var TOOL_BINDINGS = [
255
294
  // Hosted has no persistent reauthorization path for a revoked session
256
295
  // yet (TC-896), so status is local/stdio only.
257
296
  surfaces: ["local"]
297
+ },
298
+ {
299
+ name: "tinycloud_apps_context",
300
+ operationId: "tinycloud.apps.context",
301
+ operationVersion: 1,
302
+ readOnlyHint: true,
303
+ idempotentHint: true,
304
+ destructiveHint: false,
305
+ openWorldHint: true,
306
+ surfaces: ["hosted"]
307
+ },
308
+ {
309
+ name: "tinycloud_apps_read",
310
+ operationId: "tinycloud.apps.read",
311
+ operationVersion: 1,
312
+ readOnlyHint: true,
313
+ idempotentHint: true,
314
+ destructiveHint: false,
315
+ openWorldHint: true,
316
+ surfaces: ["hosted"]
317
+ },
318
+ {
319
+ name: "tinycloud_apps_write",
320
+ operationId: "tinycloud.apps.write",
321
+ operationVersion: 1,
322
+ readOnlyHint: false,
323
+ idempotentHint: false,
324
+ destructiveHint: true,
325
+ openWorldHint: true,
326
+ surfaces: ["hosted"]
258
327
  }
259
328
  ];
260
329
  function createJsonSchemaValidator() {
@@ -299,23 +368,50 @@ function registerTinyCloudTools(server, selection, validator) {
299
368
  annotations
300
369
  },
301
370
  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);
371
+ const startedAt = performance.now();
372
+ const metrics = { replayedGrants: 0 };
373
+ let code = "INTERNAL_ERROR";
374
+ try {
375
+ const result = await invokeOperation(
376
+ binding.operationId,
377
+ binding.operationVersion,
378
+ invocationTargetForMcp(selection),
379
+ input,
380
+ metrics
381
+ );
382
+ const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
383
+ const response = toMcpToolResult(projected, TOOL_BINDINGS, {
384
+ surface: selection.surface,
385
+ readOnly: binding.readOnlyHint,
386
+ operationId: binding.operationId
387
+ });
388
+ code = outcomeCode(response.structuredContent);
389
+ return response;
390
+ } finally {
391
+ if (selection.surface === "hosted") {
392
+ logToolCall(binding.name, code, startedAt, metrics.replayedGrants);
393
+ }
394
+ }
310
395
  }
311
396
  );
312
397
  }
313
398
  }
399
+ function outcomeCode(result) {
400
+ if (result.status === "error") {
401
+ const error = result.error;
402
+ return typeof error?.code === "string" && Object.hasOwn(stableErrorCodes, error.code) ? error.code : "INTERNAL_ERROR";
403
+ }
404
+ if (result.status !== "ok") return result.status;
405
+ const output = result.output;
406
+ if (output?.outcome === "COPY_PARTIAL") return "COPY_PARTIAL";
407
+ if (Array.isArray(output?.ops) && output.ops.some((op) => op.outcome !== "ok")) return "PARTIAL";
408
+ return "ok";
409
+ }
314
410
 
315
411
  // package.json
316
412
  var package_default = {
317
413
  name: "@tinycloud/mcp",
318
- version: "0.5.5-beta.0",
414
+ version: "0.6.0-beta.1",
319
415
  description: "TinyCloud delegated operations MCP server",
320
416
  keywords: [
321
417
  "tinycloud",
@@ -388,7 +484,7 @@ var package_default = {
388
484
  "@modelcontextprotocol/server": "2.0.0-beta.4",
389
485
  "@tinycloud/node-sdk": "3.2.5-beta.0",
390
486
  "@tinycloud/node-sdk-wasm": "1.7.7",
391
- "@tinycloud/operations": "0.5.5-beta.0",
487
+ "@tinycloud/operations": "0.6.0-beta.1",
392
488
  jose: "6.1.3"
393
489
  },
394
490
  devDependencies: {
@@ -633,6 +729,7 @@ import {
633
729
  } from "@tinycloud/node-sdk";
634
730
  import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk-wasm";
635
731
  import { invokeOperation as invokeOperation2 } from "@tinycloud/operations";
732
+ import { tcAppsApprovalMessage, tcAppsApprovalReason } from "@tinycloud/operations/tc-apps";
636
733
  var PROFILE = "agent";
637
734
  var MAX_CALLBACK_BYTES = 256 * 1024;
638
735
  var LEGACY_DEFAULT_NODE_HOST = "https://node.tinycloud.xyz";
@@ -685,6 +782,10 @@ var RemoteTenantStore = class {
685
782
  tenantStateRoot(subject) {
686
783
  return join2(this.#config.stateDir, "tenants", this.#tenantId(subject));
687
784
  }
785
+ /** Prepare only this authenticated account's profile before its first operation. */
786
+ async initializeTenant(subject) {
787
+ return (await this.#ensureTenant(subject)).stateRoot;
788
+ }
688
789
  async connectStatus(subject, ownerDids = []) {
689
790
  const tenant = await this.#ensureTenant(subject);
690
791
  if (await this.#hasValidSession(tenant, ownerDids)) {
@@ -699,7 +800,23 @@ var RemoteTenantStore = class {
699
800
  };
700
801
  }
701
802
  async decorateAuthorityResult(subject, ownerDids, result) {
702
- if (!isRecord(result) || result.status !== "authority_required") return result;
803
+ if (!isRecord(result)) return result;
804
+ if (result.status === "error" && isRecord(result.error) && (result.error.code === "SESSION_NOT_FOUND" || result.error.code === "AUTH_REQUIRED")) {
805
+ await this.#ensureTenant(subject);
806
+ const pending2 = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
807
+ return {
808
+ ...result,
809
+ error: {
810
+ ...result.error,
811
+ details: {
812
+ ...isRecord(result.error.details) ? result.error.details : {},
813
+ approvalUrl: this.#connectUrl(pending2),
814
+ expiresAt: pending2.expiresAt
815
+ }
816
+ }
817
+ };
818
+ }
819
+ if (result.status !== "authority_required") return result;
703
820
  const request = isRecord(result.request) ? result.request : void 0;
704
821
  const requestId = typeof request?.requestId === "string" ? request.requestId : void 0;
705
822
  const requested = Array.isArray(request?.requested) ? request.requested.filter(isPermissionEntry) : [];
@@ -712,7 +829,7 @@ var RemoteTenantStore = class {
712
829
  kind: "openkey",
713
830
  requestId,
714
831
  url: this.#connectUrl(pending),
715
- fallback: "Open the approval URL, then retry the same MCP tool."
832
+ fallback: tcAppsApprovalMessage(requested) ?? "Open the approval URL, then retry the same MCP tool."
716
833
  }
717
834
  };
718
835
  }
@@ -729,7 +846,7 @@ var RemoteTenantStore = class {
729
846
  permissions: Buffer.from(JSON.stringify({
730
847
  permissions: pending.permissions,
731
848
  reason: [
732
- pending.kind === "bootstrap" ? "Connect this TinyCloud account to the hosted MCP delegate." : "Approve the exact capabilities requested by the TinyCloud MCP operation.",
849
+ 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
850
  // Owner DIDs come from the verified OpenKey access token of the MCP
734
851
  // client that created this link, never from the link itself.
735
852
  ...pending.ownerDids.length === 0 ? [] : [`Requested by TinyCloud account ${pending.ownerDids.join(", ")}; approve only if that is you.`]
@@ -1167,13 +1284,13 @@ function createHostedMcpApp(config) {
1167
1284
  const handler = createMcpHandler(async ({ authInfo: authInfo2 }) => {
1168
1285
  const identity = oauthIdentity(authInfo2);
1169
1286
  const { subject, ownerDids } = identity;
1170
- const stateRoot = store.tenantStateRoot(subject);
1287
+ const stateRoot = await store.initializeTenant(subject);
1171
1288
  const validator = createJsonSchemaValidator();
1172
1289
  const server = new McpServer2(
1173
1290
  { name: MCP_SERVER_NAME, version: MCP_VERSION },
1174
1291
  {
1175
1292
  jsonSchemaValidator: validator,
1176
- instructions: "Call tinycloud_connect first. If an operation returns authority_required, open approval.url, then retry that operation unchanged."
1293
+ 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
1294
  }
1178
1295
  );
1179
1296
  registerTinyCloudTools(server, {
@@ -1285,14 +1402,21 @@ function registerConnectTool(server, validator, store, identity) {
1285
1402
  }
1286
1403
  },
1287
1404
  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
- };
1405
+ const startedAt = performance.now();
1406
+ let code = "INTERNAL_ERROR";
1407
+ try {
1408
+ const status = await store.connectStatus(identity.subject, identity.ownerDids);
1409
+ code = status.connected ? "ok" : "authority_required";
1410
+ return {
1411
+ content: [{
1412
+ type: "text",
1413
+ text: status.connected ? "TinyCloud is connected." : `TinyCloud approval is required. Open ${status.approvalUrl}, then call this tool again. This link expires at ${status.expiresAt}.`
1414
+ }],
1415
+ structuredContent: status
1416
+ };
1417
+ } finally {
1418
+ logToolCall("tinycloud_connect", code, startedAt, 0);
1419
+ }
1296
1420
  }
1297
1421
  );
1298
1422
  }