@tinycloud/mcp 0.4.1-beta.0 → 0.5.0-beta.10
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/README.md +27 -5
- package/dist/cli.cjs +40 -8
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +40 -8
- package/dist/cli.js.map +1 -1
- package/dist/http-cli.cjs +86 -81
- package/dist/http-cli.cjs.map +1 -1
- package/dist/http-cli.js +85 -78
- package/dist/http-cli.js.map +1 -1
- package/dist/http.cjs +86 -81
- package/dist/http.cjs.map +1 -1
- package/dist/http.js +85 -78
- package/dist/http.js.map +1 -1
- package/dist/index.cjs +89 -83
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +88 -80
- package/dist/index.js.map +1 -1
- package/dist/results.cjs +14 -2
- package/dist/results.cjs.map +1 -1
- package/dist/results.d.cts +7 -2
- package/dist/results.d.ts +7 -2
- package/dist/results.js +14 -2
- package/dist/results.js.map +1 -1
- package/dist/server.cjs +40 -8
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +5 -3
- package/dist/server.d.ts +5 -3
- package/dist/server.js +40 -8
- package/dist/server.js.map +1 -1
- package/dist/tools.cjs +42 -5
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.d.cts +12 -3
- package/dist/tools.d.ts +12 -3
- package/dist/tools.js +40 -5
- package/dist/tools.js.map +1 -1
- package/generated/mcp-facts.json +80 -0
- package/package.json +3 -3
- package/skills/tinycloud-delegated-secrets/SKILL.md +26 -18
package/dist/index.js
CHANGED
|
@@ -14,8 +14,8 @@ 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) {
|
|
18
|
-
const canonical = isCanonicalOperationResult(result) ? result : {
|
|
17
|
+
function toMcpToolResult(result, bindings = []) {
|
|
18
|
+
const canonical = isCanonicalOperationResult(result) ? withRecoveryTool(result, bindings) : {
|
|
19
19
|
status: "error",
|
|
20
20
|
operation: {},
|
|
21
21
|
context: {},
|
|
@@ -26,6 +26,18 @@ function toMcpToolResult(result) {
|
|
|
26
26
|
structuredContent: canonical
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
+
function withRecoveryTool(result, bindings) {
|
|
30
|
+
if (result.status !== "error") return result;
|
|
31
|
+
const error = result.error;
|
|
32
|
+
const recovery = error?.details?.recovery;
|
|
33
|
+
if (recovery?.action !== "complete_migration") return result;
|
|
34
|
+
const binding = bindings.find((candidate) => candidate.operationId === recovery.operationId);
|
|
35
|
+
if (binding === void 0) return result;
|
|
36
|
+
return {
|
|
37
|
+
...result,
|
|
38
|
+
error: { ...error, details: { ...error?.details, recovery: { ...recovery, tool: binding.name } } }
|
|
39
|
+
};
|
|
40
|
+
}
|
|
29
41
|
function canonicalMcpAdapterResult(result) {
|
|
30
42
|
return toMcpToolResult(result).structuredContent;
|
|
31
43
|
}
|
|
@@ -64,7 +76,8 @@ var TOOL_NAMES = [
|
|
|
64
76
|
"tinycloud_sql_schema_inspect",
|
|
65
77
|
"tinycloud_sql_query",
|
|
66
78
|
"tinycloud_sql_execute",
|
|
67
|
-
"tinycloud_secrets_get"
|
|
79
|
+
"tinycloud_secrets_get",
|
|
80
|
+
"tinycloud_secrets_status"
|
|
68
81
|
];
|
|
69
82
|
function invocationTargetForMcp(selection) {
|
|
70
83
|
return {
|
|
@@ -79,6 +92,10 @@ var packageRequire = createRequire(
|
|
|
79
92
|
var generatedCatalog = packageRequire(
|
|
80
93
|
"@tinycloud/operations/operations.json"
|
|
81
94
|
);
|
|
95
|
+
var ALL_SURFACES = ["local", "hosted"];
|
|
96
|
+
function bindingSurfaces(binding) {
|
|
97
|
+
return binding.surfaces ?? ALL_SURFACES;
|
|
98
|
+
}
|
|
82
99
|
var TOOL_BINDINGS = [
|
|
83
100
|
{
|
|
84
101
|
name: "tinycloud_status",
|
|
@@ -123,7 +140,10 @@ var TOOL_BINDINGS = [
|
|
|
123
140
|
readOnlyHint: false,
|
|
124
141
|
idempotentHint: true,
|
|
125
142
|
destructiveHint: false,
|
|
126
|
-
openWorldHint: false
|
|
143
|
+
openWorldHint: false,
|
|
144
|
+
// Hosted delegations must arrive through the owner/request-bound OpenKey callback,
|
|
145
|
+
// not a caller-supplied import artifact.
|
|
146
|
+
surfaces: ["local"]
|
|
127
147
|
},
|
|
128
148
|
{
|
|
129
149
|
name: "tinycloud_account_spaces_list",
|
|
@@ -223,6 +243,18 @@ var TOOL_BINDINGS = [
|
|
|
223
243
|
idempotentHint: true,
|
|
224
244
|
destructiveHint: false,
|
|
225
245
|
openWorldHint: true
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: "tinycloud_secrets_status",
|
|
249
|
+
operationId: "tinycloud.secrets.status",
|
|
250
|
+
operationVersion: 1,
|
|
251
|
+
readOnlyHint: true,
|
|
252
|
+
idempotentHint: true,
|
|
253
|
+
destructiveHint: false,
|
|
254
|
+
openWorldHint: true,
|
|
255
|
+
// Hosted has no persistent reauthorization path for a revoked session
|
|
256
|
+
// yet (TC-896), so status is local/stdio only.
|
|
257
|
+
surfaces: ["local"]
|
|
226
258
|
}
|
|
227
259
|
];
|
|
228
260
|
function createJsonSchemaValidator() {
|
|
@@ -238,6 +270,7 @@ function registerTinyCloudTools(server, selection, validator) {
|
|
|
238
270
|
])
|
|
239
271
|
);
|
|
240
272
|
for (const binding of TOOL_BINDINGS) {
|
|
273
|
+
if (!bindingSurfaces(binding).includes(selection.surface)) continue;
|
|
241
274
|
const operation = definitions.get(`${binding.operationId}@${binding.operationVersion}`);
|
|
242
275
|
if (operation === void 0) {
|
|
243
276
|
throw new Error(`Generated operations catalog is missing ${binding.operationId}@${binding.operationVersion}.`);
|
|
@@ -273,7 +306,7 @@ function registerTinyCloudTools(server, selection, validator) {
|
|
|
273
306
|
input
|
|
274
307
|
);
|
|
275
308
|
const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
|
|
276
|
-
return toMcpToolResult(projected);
|
|
309
|
+
return toMcpToolResult(projected, TOOL_BINDINGS);
|
|
277
310
|
}
|
|
278
311
|
);
|
|
279
312
|
}
|
|
@@ -282,7 +315,7 @@ function registerTinyCloudTools(server, selection, validator) {
|
|
|
282
315
|
// package.json
|
|
283
316
|
var package_default = {
|
|
284
317
|
name: "@tinycloud/mcp",
|
|
285
|
-
version: "0.
|
|
318
|
+
version: "0.5.0-beta.10",
|
|
286
319
|
description: "TinyCloud delegated operations MCP server",
|
|
287
320
|
keywords: [
|
|
288
321
|
"tinycloud",
|
|
@@ -353,9 +386,9 @@ var package_default = {
|
|
|
353
386
|
dependencies: {
|
|
354
387
|
"@modelcontextprotocol/node": "2.0.0-beta.4",
|
|
355
388
|
"@modelcontextprotocol/server": "2.0.0-beta.4",
|
|
356
|
-
"@tinycloud/node-sdk": "3.2.0-beta.
|
|
389
|
+
"@tinycloud/node-sdk": "3.2.0-beta.9",
|
|
357
390
|
"@tinycloud/node-sdk-wasm": "1.7.7",
|
|
358
|
-
"@tinycloud/operations": "0.5.0-beta.
|
|
391
|
+
"@tinycloud/operations": "0.5.0-beta.10",
|
|
359
392
|
jose: "6.1.3"
|
|
360
393
|
},
|
|
361
394
|
devDependencies: {
|
|
@@ -382,7 +415,7 @@ function createTinyCloudMcpServer(selection) {
|
|
|
382
415
|
{ name: MCP_SERVER_NAME, version: MCP_VERSION },
|
|
383
416
|
{ jsonSchemaValidator: validator }
|
|
384
417
|
);
|
|
385
|
-
registerTinyCloudTools(server, selection, validator);
|
|
418
|
+
registerTinyCloudTools(server, { ...selection, surface: "local" }, validator);
|
|
386
419
|
return server;
|
|
387
420
|
}
|
|
388
421
|
function serveTinyCloudMcp(selection) {
|
|
@@ -509,6 +542,7 @@ import {
|
|
|
509
542
|
OAuthError,
|
|
510
543
|
OAuthErrorCode
|
|
511
544
|
} from "@modelcontextprotocol/server";
|
|
545
|
+
import { pkhDid } from "@tinycloud/node-sdk";
|
|
512
546
|
import { createRemoteJWKSet, errors, jwtVerify } from "jose";
|
|
513
547
|
var OWNER_DIDS_CLAIM = "https://tinycloud.xyz/owner_dids";
|
|
514
548
|
function createOpenKeyTokenVerifier(config) {
|
|
@@ -545,11 +579,11 @@ function authInfo(token, payload, config) {
|
|
|
545
579
|
);
|
|
546
580
|
}
|
|
547
581
|
const clientId = firstString(payload.azp, payload.client_id) ?? "unknown";
|
|
548
|
-
const ownerDids =
|
|
549
|
-
if (ownerDids
|
|
582
|
+
const ownerDids = canonicalOwnerDids(payload[OWNER_DIDS_CLAIM]);
|
|
583
|
+
if (ownerDids === void 0) {
|
|
550
584
|
throw new OAuthError(
|
|
551
585
|
OAuthErrorCode.InvalidToken,
|
|
552
|
-
"The OpenKey access token has no TinyCloud owner identity."
|
|
586
|
+
"The OpenKey access token has no valid TinyCloud owner identity."
|
|
553
587
|
);
|
|
554
588
|
}
|
|
555
589
|
return {
|
|
@@ -564,6 +598,21 @@ function authInfo(token, payload, config) {
|
|
|
564
598
|
}
|
|
565
599
|
};
|
|
566
600
|
}
|
|
601
|
+
var OWNER_DID = /^did:pkh:eip155:([1-9][0-9]{0,15}):(0x[0-9a-fA-F]{40})$/;
|
|
602
|
+
function canonicalOwnerDids(claim) {
|
|
603
|
+
if (!Array.isArray(claim) || claim.length === 0) return void 0;
|
|
604
|
+
const ownerDids = [];
|
|
605
|
+
for (const value of claim) {
|
|
606
|
+
const match = typeof value === "string" ? OWNER_DID.exec(value) : null;
|
|
607
|
+
if (match === null) return void 0;
|
|
608
|
+
try {
|
|
609
|
+
ownerDids.push(pkhDid(match[2], Number(match[1])));
|
|
610
|
+
} catch {
|
|
611
|
+
return void 0;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return [...new Set(ownerDids)];
|
|
615
|
+
}
|
|
567
616
|
function firstString(...values) {
|
|
568
617
|
return values.find((value) => typeof value === "string" && value.length > 0);
|
|
569
618
|
}
|
|
@@ -574,7 +623,9 @@ import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promi
|
|
|
574
623
|
import { dirname, join as join2 } from "path";
|
|
575
624
|
import {
|
|
576
625
|
TinyCloudNode,
|
|
577
|
-
|
|
626
|
+
isCapabilitySubset,
|
|
627
|
+
principalDidEquals,
|
|
628
|
+
siweRuntimeDelegation
|
|
578
629
|
} from "@tinycloud/node-sdk";
|
|
579
630
|
import { TCWSessionManager, importKey, initPanicHook } from "@tinycloud/node-sdk-wasm";
|
|
580
631
|
import { invokeOperation as invokeOperation2 } from "@tinycloud/operations";
|
|
@@ -689,8 +740,7 @@ var RemoteTenantStore = class {
|
|
|
689
740
|
if (pending.ownerDids.length > 0 && !pending.ownerDids.some((did) => samePrincipal(node.did, did))) {
|
|
690
741
|
throw new ApprovalOwnerMismatchError();
|
|
691
742
|
}
|
|
692
|
-
|
|
693
|
-
if (!permissionsCover(pending.permissions, granted)) {
|
|
743
|
+
if (!isCapabilitySubset(pending.permissions, node.getVerifiedSessionCapabilities()).subset) {
|
|
694
744
|
throw new Error("The approved delegation does not cover the requested account access.");
|
|
695
745
|
}
|
|
696
746
|
const profile = await readJson(this.#profilePath(tenant.stateRoot));
|
|
@@ -706,14 +756,25 @@ var RemoteTenantStore = class {
|
|
|
706
756
|
}
|
|
707
757
|
async #storeDelegation(tenant, pending, data) {
|
|
708
758
|
if (pending.requestId === void 0) throw new Error("Authority request is missing.");
|
|
709
|
-
const { node } = await this.#restoreApproval(tenant, data);
|
|
759
|
+
const { node, session } = await this.#restoreApproval(tenant, data);
|
|
710
760
|
if (!pending.ownerDids.some((did) => samePrincipal(node.did, did))) throw new ApprovalOwnerMismatchError();
|
|
711
|
-
const delegation =
|
|
761
|
+
const delegation = siweRuntimeDelegation(node, {
|
|
762
|
+
authorization: session.delegationHeader.Authorization,
|
|
763
|
+
cid: session.delegationCid,
|
|
764
|
+
spaceId: session.spaceId,
|
|
765
|
+
ownerAddress: session.address,
|
|
766
|
+
chainId: session.chainId,
|
|
767
|
+
siwe: session.siwe,
|
|
768
|
+
signature: session.signature,
|
|
769
|
+
host: this.#config.nodeHost
|
|
770
|
+
});
|
|
712
771
|
const result = await invokeOperation2(
|
|
713
772
|
"tinycloud.auth.import",
|
|
714
773
|
1,
|
|
715
774
|
{ profile: PROFILE, stateRoot: tenant.stateRoot },
|
|
716
775
|
{
|
|
776
|
+
kind: "tinycloud.auth.delegation",
|
|
777
|
+
version: 1,
|
|
717
778
|
requestId: pending.requestId,
|
|
718
779
|
delegationCid: delegation.cid,
|
|
719
780
|
delegation
|
|
@@ -723,8 +784,10 @@ var RemoteTenantStore = class {
|
|
|
723
784
|
}
|
|
724
785
|
/**
|
|
725
786
|
* Verify the OpenKey callback as a SIWE session for this tenant's key. The
|
|
726
|
-
*
|
|
727
|
-
*
|
|
787
|
+
* callback must carry the full signed proof (SIWE, signature, address and
|
|
788
|
+
* chain), so the WASM verifier always runs: it binds the authorization bytes
|
|
789
|
+
* and CID to the signed SIWE, and `node.did` is the wallet that signed
|
|
790
|
+
* exactly the delegation being stored.
|
|
728
791
|
*/
|
|
729
792
|
async #restoreApproval(tenant, data) {
|
|
730
793
|
const session = sessionFromCallback(data, tenant.jwk);
|
|
@@ -904,7 +967,7 @@ function ensureWasm() {
|
|
|
904
967
|
}
|
|
905
968
|
function sessionFromCallback(data, privateJwk) {
|
|
906
969
|
const delegationHeader = data.delegationHeader;
|
|
907
|
-
if (!isRecord(delegationHeader) || typeof delegationHeader.Authorization !== "string" || typeof data.delegationCid !== "string" || typeof data.spaceId !== "string" || typeof data.verificationMethod !== "string") {
|
|
970
|
+
if (!isRecord(delegationHeader) || typeof delegationHeader.Authorization !== "string" || typeof data.delegationCid !== "string" || typeof data.spaceId !== "string" || typeof data.verificationMethod !== "string" || typeof data.address !== "string" || typeof data.chainId !== "number" || typeof data.siwe !== "string" || typeof data.signature !== "string") {
|
|
908
971
|
throw new Error("OpenKey returned an incomplete delegation.");
|
|
909
972
|
}
|
|
910
973
|
return {
|
|
@@ -913,63 +976,12 @@ function sessionFromCallback(data, privateJwk) {
|
|
|
913
976
|
spaceId: data.spaceId,
|
|
914
977
|
jwk: privateJwk,
|
|
915
978
|
verificationMethod: data.verificationMethod,
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
979
|
+
address: data.address,
|
|
980
|
+
chainId: data.chainId,
|
|
981
|
+
siwe: data.siwe,
|
|
982
|
+
signature: data.signature
|
|
920
983
|
};
|
|
921
984
|
}
|
|
922
|
-
function portableFromCallback(data, permissions, host) {
|
|
923
|
-
const primary = permissions.find((permission) => permission.service !== "tinycloud.encryption") ?? permissions[0];
|
|
924
|
-
if (primary === void 0) throw new Error("The authority request has no permissions.");
|
|
925
|
-
const session = sessionFromCallback(data, {});
|
|
926
|
-
const expiry = delegationExpiry(data);
|
|
927
|
-
return {
|
|
928
|
-
cid: session.delegationCid,
|
|
929
|
-
delegationHeader: session.delegationHeader,
|
|
930
|
-
spaceId: session.spaceId,
|
|
931
|
-
path: primary.path,
|
|
932
|
-
actions: [...primary.actions],
|
|
933
|
-
resources: permissions.map((permission) => ({
|
|
934
|
-
service: permission.service.startsWith("tinycloud.") ? permission.service.slice("tinycloud.".length) : permission.service,
|
|
935
|
-
space: permission.service === "tinycloud.encryption" ? requiredPermissionSpace(permission) : session.spaceId,
|
|
936
|
-
path: permission.path,
|
|
937
|
-
actions: [...permission.actions]
|
|
938
|
-
})),
|
|
939
|
-
expiry,
|
|
940
|
-
delegateDID: session.verificationMethod,
|
|
941
|
-
ownerAddress: typeof data.address === "string" ? data.address : "",
|
|
942
|
-
chainId: typeof data.chainId === "number" ? data.chainId : 1,
|
|
943
|
-
host
|
|
944
|
-
};
|
|
945
|
-
}
|
|
946
|
-
function delegationExpiry(data) {
|
|
947
|
-
for (const key of ["expiry", "expiresAt", "expirationTime"]) {
|
|
948
|
-
const parsed = parseDate(data[key]);
|
|
949
|
-
if (parsed !== void 0) return parsed;
|
|
950
|
-
}
|
|
951
|
-
if (typeof data.siwe === "string") {
|
|
952
|
-
const match = data.siwe.match(/^Expiration Time:\s*(.+)$/im);
|
|
953
|
-
const parsed = parseDate(match?.[1]?.trim());
|
|
954
|
-
if (parsed !== void 0) return parsed;
|
|
955
|
-
}
|
|
956
|
-
throw new Error("OpenKey did not return a delegation expiry.");
|
|
957
|
-
}
|
|
958
|
-
function permissionsCover(requested, granted) {
|
|
959
|
-
return requested.every((request) => granted.some(
|
|
960
|
-
(grant) => normalizeService(grant.service) === normalizeService(request.service) && typeof grant.space === "string" && typeof request.space === "string" && sameSpace(grant.space, request.space) && grant.path === request.path && request.actions.every((action) => grant.actions.includes(action))
|
|
961
|
-
));
|
|
962
|
-
}
|
|
963
|
-
function sameSpace(granted, requested) {
|
|
964
|
-
return granted === requested || granted.endsWith(`:${requested}`);
|
|
965
|
-
}
|
|
966
|
-
function normalizeService(service) {
|
|
967
|
-
return service.startsWith("tinycloud.") ? service : `tinycloud.${service}`;
|
|
968
|
-
}
|
|
969
|
-
function requiredPermissionSpace(permission) {
|
|
970
|
-
if (typeof permission.space !== "string") throw new Error("The authority request has no space.");
|
|
971
|
-
return permission.space;
|
|
972
|
-
}
|
|
973
985
|
function publicJwk(jwk) {
|
|
974
986
|
const copy = { ...jwk };
|
|
975
987
|
for (const field of ["d", "p", "q", "dp", "dq", "qi", "oth", "k"]) delete copy[field];
|
|
@@ -992,11 +1004,6 @@ function isPermissionEntry(value) {
|
|
|
992
1004
|
function isRecord(value) {
|
|
993
1005
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
994
1006
|
}
|
|
995
|
-
function parseDate(value) {
|
|
996
|
-
if (typeof value !== "string" && typeof value !== "number") return void 0;
|
|
997
|
-
const date = new Date(typeof value === "number" && value < 1e10 ? value * 1e3 : value);
|
|
998
|
-
return Number.isNaN(date.getTime()) ? void 0 : date;
|
|
999
|
-
}
|
|
1000
1007
|
async function secureDirectory(path) {
|
|
1001
1008
|
await mkdir(path, { recursive: true, mode: 448 });
|
|
1002
1009
|
await chmod(path, 448);
|
|
@@ -1086,6 +1093,7 @@ function createHostedMcpApp(config) {
|
|
|
1086
1093
|
}
|
|
1087
1094
|
);
|
|
1088
1095
|
registerTinyCloudTools(server, {
|
|
1096
|
+
surface: "hosted",
|
|
1089
1097
|
profile: "agent",
|
|
1090
1098
|
explicitProfile: true,
|
|
1091
1099
|
allowOwnerProfile: false,
|