@tinycloud/mcp 0.2.2-beta.0 → 0.3.0-beta.0
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 +38 -4
- package/bin/tinycloud-mcp-http +2 -0
- package/dist/cli.cjs +18 -5
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +18 -5
- package/dist/cli.js.map +1 -1
- package/dist/http-cli.cjs +1179 -0
- package/dist/http-cli.cjs.map +1 -0
- package/dist/http-cli.d.cts +3 -0
- package/dist/http-cli.d.ts +3 -0
- package/dist/http-cli.js +1167 -0
- package/dist/http-cli.js.map +1 -0
- package/dist/http.cjs +1093 -0
- package/dist/http.cjs.map +1 -0
- package/dist/http.d.cts +23 -0
- package/dist/http.d.ts +23 -0
- package/dist/http.js +1081 -0
- package/dist/http.js.map +1 -0
- package/dist/index.cjs +730 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +743 -5
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +18 -5
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +18 -5
- package/dist/server.js.map +1 -1
- package/dist/tools.cjs +4 -2
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.d.cts +5 -0
- package/dist/tools.d.ts +5 -0
- package/dist/tools.js +4 -2
- package/dist/tools.js.map +1 -1
- package/package.json +14 -3
package/dist/http.cjs
ADDED
|
@@ -0,0 +1,1093 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/http.ts
|
|
21
|
+
var http_exports = {};
|
|
22
|
+
__export(http_exports, {
|
|
23
|
+
createHostedMcpApp: () => createHostedMcpApp
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(http_exports);
|
|
26
|
+
var import_server4 = require("@modelcontextprotocol/server");
|
|
27
|
+
|
|
28
|
+
// src/oauth.ts
|
|
29
|
+
var import_server = require("@modelcontextprotocol/server");
|
|
30
|
+
var import_jose = require("jose");
|
|
31
|
+
var OWNER_DIDS_CLAIM = "https://tinycloud.xyz/owner_dids";
|
|
32
|
+
function createOpenKeyTokenVerifier(config) {
|
|
33
|
+
const jwks = (0, import_jose.createRemoteJWKSet)(new URL(config.jwksUri));
|
|
34
|
+
return {
|
|
35
|
+
async verifyAccessToken(token) {
|
|
36
|
+
try {
|
|
37
|
+
const { payload } = await (0, import_jose.jwtVerify)(token, jwks, {
|
|
38
|
+
issuer: config.issuer,
|
|
39
|
+
audience: config.audience
|
|
40
|
+
});
|
|
41
|
+
return authInfo(token, payload, config);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error instanceof import_jose.errors.JOSEError) {
|
|
44
|
+
throw new import_server.OAuthError(import_server.OAuthErrorCode.InvalidToken, "The OpenKey access token is invalid.");
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function authInfo(token, payload, config) {
|
|
52
|
+
if (typeof payload.sub !== "string" || payload.sub.length === 0) {
|
|
53
|
+
throw new import_server.OAuthError(import_server.OAuthErrorCode.InvalidToken, "The OpenKey access token does not identify a subject.");
|
|
54
|
+
}
|
|
55
|
+
if (typeof payload.exp !== "number") {
|
|
56
|
+
throw new import_server.OAuthError(import_server.OAuthErrorCode.InvalidToken, "The OpenKey access token has no expiry.");
|
|
57
|
+
}
|
|
58
|
+
const scopes = typeof payload.scope === "string" ? payload.scope.split(/\s+/).filter(Boolean) : [];
|
|
59
|
+
if (config.requiredScope !== void 0 && !scopes.includes(config.requiredScope)) {
|
|
60
|
+
throw new import_server.OAuthError(
|
|
61
|
+
import_server.OAuthErrorCode.InsufficientScope,
|
|
62
|
+
"The OpenKey access token is missing the TinyCloud MCP scope."
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const clientId = firstString(payload.azp, payload.client_id) ?? "unknown";
|
|
66
|
+
const ownerDids = Array.isArray(payload[OWNER_DIDS_CLAIM]) ? payload[OWNER_DIDS_CLAIM].filter((value) => typeof value === "string") : [];
|
|
67
|
+
if (ownerDids.length === 0) {
|
|
68
|
+
throw new import_server.OAuthError(
|
|
69
|
+
import_server.OAuthErrorCode.InvalidToken,
|
|
70
|
+
"The OpenKey access token has no TinyCloud owner identity."
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
token,
|
|
75
|
+
clientId,
|
|
76
|
+
scopes,
|
|
77
|
+
expiresAt: payload.exp,
|
|
78
|
+
resource: new URL(config.audience),
|
|
79
|
+
extra: {
|
|
80
|
+
subject: payload.sub,
|
|
81
|
+
ownerDids
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function firstString(...values) {
|
|
86
|
+
return values.find((value) => typeof value === "string" && value.length > 0);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/remote-store.ts
|
|
90
|
+
var import_node_crypto = require("crypto");
|
|
91
|
+
var import_promises = require("fs/promises");
|
|
92
|
+
var import_node_path = require("path");
|
|
93
|
+
var import_node_sdk = require("@tinycloud/node-sdk");
|
|
94
|
+
var import_node_sdk_wasm = require("@tinycloud/node-sdk-wasm");
|
|
95
|
+
var import_operations = require("@tinycloud/operations");
|
|
96
|
+
var PROFILE = "agent";
|
|
97
|
+
var MAX_CALLBACK_BYTES = 256 * 1024;
|
|
98
|
+
var BOOTSTRAP_PERMISSIONS = [
|
|
99
|
+
{
|
|
100
|
+
service: "tinycloud.kv",
|
|
101
|
+
space: "account",
|
|
102
|
+
path: "spaces/",
|
|
103
|
+
actions: ["tinycloud.kv/get", "tinycloud.kv/list"]
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
service: "tinycloud.kv",
|
|
107
|
+
space: "account",
|
|
108
|
+
path: "applications/",
|
|
109
|
+
actions: ["tinycloud.kv/get", "tinycloud.kv/list"]
|
|
110
|
+
}
|
|
111
|
+
];
|
|
112
|
+
var RemoteTenantStore = class {
|
|
113
|
+
#config;
|
|
114
|
+
#initializing = /* @__PURE__ */ new Map();
|
|
115
|
+
constructor(config) {
|
|
116
|
+
this.#config = config;
|
|
117
|
+
}
|
|
118
|
+
tenantStateRoot(subject) {
|
|
119
|
+
return (0, import_node_path.join)(this.#config.stateDir, "tenants", this.#tenantId(subject));
|
|
120
|
+
}
|
|
121
|
+
async connectStatus(subject, ownerDids = []) {
|
|
122
|
+
const tenant = await this.#ensureTenant(subject);
|
|
123
|
+
if (await this.#hasValidSession(tenant, ownerDids)) {
|
|
124
|
+
return { connected: true, sessionDid: tenant.sessionDid };
|
|
125
|
+
}
|
|
126
|
+
const pending = await this.#createPending(subject, ownerDids, "bootstrap", BOOTSTRAP_PERMISSIONS);
|
|
127
|
+
return {
|
|
128
|
+
connected: false,
|
|
129
|
+
sessionDid: tenant.sessionDid,
|
|
130
|
+
approvalUrl: this.#connectUrl(pending),
|
|
131
|
+
expiresAt: pending.expiresAt
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
async decorateAuthorityResult(subject, ownerDids, result) {
|
|
135
|
+
if (!isRecord(result) || result.status !== "authority_required") return result;
|
|
136
|
+
const request = isRecord(result.request) ? result.request : void 0;
|
|
137
|
+
const requestId = typeof request?.requestId === "string" ? request.requestId : void 0;
|
|
138
|
+
const requested = Array.isArray(request?.requested) ? request.requested.filter(isPermissionEntry) : [];
|
|
139
|
+
if (requestId === void 0 || requested.length === 0) return result;
|
|
140
|
+
const pending = await this.#createPending(subject, ownerDids, "delegation", requested, requestId);
|
|
141
|
+
return {
|
|
142
|
+
...result,
|
|
143
|
+
approval: {
|
|
144
|
+
...isRecord(result.approval) ? result.approval : {},
|
|
145
|
+
kind: "openkey",
|
|
146
|
+
requestId,
|
|
147
|
+
url: this.#connectUrl(pending),
|
|
148
|
+
fallback: "Open the approval URL, then retry the same MCP tool."
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async approvalRedirect(state) {
|
|
153
|
+
const pending = await this.#readPending(state, false);
|
|
154
|
+
const tenant = await this.#readTenant(pending.tenantId);
|
|
155
|
+
const callback = new URL("/connect/callback", this.#config.publicUrl);
|
|
156
|
+
callback.searchParams.set("state", state);
|
|
157
|
+
const params = new URLSearchParams({
|
|
158
|
+
did: tenant.sessionDid,
|
|
159
|
+
callback: callback.toString(),
|
|
160
|
+
host: this.#config.nodeHost,
|
|
161
|
+
jwk: Buffer.from(JSON.stringify(publicJwk(tenant.jwk))).toString("base64url"),
|
|
162
|
+
permissions: Buffer.from(JSON.stringify({
|
|
163
|
+
permissions: pending.permissions,
|
|
164
|
+
reason: pending.kind === "bootstrap" ? "Connect this TinyCloud account to the hosted MCP delegate." : "Approve the exact capabilities requested by the TinyCloud MCP operation."
|
|
165
|
+
})).toString("base64url"),
|
|
166
|
+
expiry: this.#config.delegationExpiry
|
|
167
|
+
});
|
|
168
|
+
return `${this.#config.openkeyHost.replace(/\/+$/, "")}/delegate?${params.toString()}`;
|
|
169
|
+
}
|
|
170
|
+
async completeApproval(state, request) {
|
|
171
|
+
const length = Number(request.headers.get("content-length") ?? "0");
|
|
172
|
+
if (Number.isFinite(length) && length > MAX_CALLBACK_BYTES) throw new Error("Callback body is too large.");
|
|
173
|
+
const text = await request.text();
|
|
174
|
+
if (Buffer.byteLength(text) > MAX_CALLBACK_BYTES) throw new Error("Callback body is too large.");
|
|
175
|
+
const data = JSON.parse(text);
|
|
176
|
+
if (!isRecord(data)) throw new Error("Callback body is invalid.");
|
|
177
|
+
const pending = await this.#readPending(state, true);
|
|
178
|
+
try {
|
|
179
|
+
const tenant = await this.#readTenant(pending.tenantId);
|
|
180
|
+
if (pending.kind === "bootstrap") {
|
|
181
|
+
await this.#storeBootstrap(tenant, pending, data);
|
|
182
|
+
} else {
|
|
183
|
+
await this.#storeDelegation(tenant, pending, data);
|
|
184
|
+
}
|
|
185
|
+
await (0, import_promises.rm)(this.#pendingPath(tenant.stateRoot, pending.nonce) + ".processing", { force: true });
|
|
186
|
+
} catch (error) {
|
|
187
|
+
await this.#restorePending(pending).catch(() => void 0);
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async #storeBootstrap(tenant, pending, data) {
|
|
192
|
+
const session = sessionFromCallback(data, tenant.jwk);
|
|
193
|
+
const node = new import_node_sdk.TinyCloudNode({ host: this.#config.nodeHost });
|
|
194
|
+
await node.restoreSession(session);
|
|
195
|
+
if (!samePrincipal(node.sessionDid, tenant.sessionDid)) {
|
|
196
|
+
throw new Error("The approved delegation targets a different session.");
|
|
197
|
+
}
|
|
198
|
+
if (pending.ownerDids.length > 0 && !pending.ownerDids.some((did) => samePrincipal(node.did, did))) {
|
|
199
|
+
throw new Error("The approved delegation belongs to a different OpenKey user.");
|
|
200
|
+
}
|
|
201
|
+
const granted = [...node.getVerifiedSessionCapabilities()];
|
|
202
|
+
if (!permissionsCover(pending.permissions, granted)) {
|
|
203
|
+
throw new Error("The approved delegation does not cover the requested account access.");
|
|
204
|
+
}
|
|
205
|
+
const profile = await readJson(this.#profilePath(tenant.stateRoot));
|
|
206
|
+
await secureWrite(this.#profilePath(tenant.stateRoot), {
|
|
207
|
+
...profile,
|
|
208
|
+
ownerDid: normalizeDid(node.did),
|
|
209
|
+
spaceId: session.spaceId
|
|
210
|
+
});
|
|
211
|
+
await secureWrite(this.#sessionPath(tenant.stateRoot), {
|
|
212
|
+
authMethod: "openkey",
|
|
213
|
+
...session
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
async #storeDelegation(tenant, pending, data) {
|
|
217
|
+
if (pending.requestId === void 0) throw new Error("Authority request is missing.");
|
|
218
|
+
const delegation = portableFromCallback(data, pending.permissions, this.#config.nodeHost);
|
|
219
|
+
const result = await (0, import_operations.invokeOperation)(
|
|
220
|
+
"tinycloud.auth.import",
|
|
221
|
+
1,
|
|
222
|
+
{ profile: PROFILE, stateRoot: tenant.stateRoot },
|
|
223
|
+
{
|
|
224
|
+
requestId: pending.requestId,
|
|
225
|
+
delegationCid: delegation.cid,
|
|
226
|
+
delegation
|
|
227
|
+
}
|
|
228
|
+
);
|
|
229
|
+
if (result.status !== "ok") throw new Error("TinyCloud rejected the approved delegation.");
|
|
230
|
+
}
|
|
231
|
+
async #ensureTenant(subject) {
|
|
232
|
+
const tenantId = this.#tenantId(subject);
|
|
233
|
+
const current = this.#initializing.get(tenantId);
|
|
234
|
+
if (current !== void 0) return current;
|
|
235
|
+
const initializing = this.#initializeTenant(tenantId).finally(() => {
|
|
236
|
+
this.#initializing.delete(tenantId);
|
|
237
|
+
});
|
|
238
|
+
this.#initializing.set(tenantId, initializing);
|
|
239
|
+
return initializing;
|
|
240
|
+
}
|
|
241
|
+
async #hasValidSession(tenant, ownerDids) {
|
|
242
|
+
const raw = await readJson(this.#sessionPath(tenant.stateRoot));
|
|
243
|
+
if (raw === null) return false;
|
|
244
|
+
try {
|
|
245
|
+
const node = new import_node_sdk.TinyCloudNode({ host: this.#config.nodeHost });
|
|
246
|
+
await node.restoreSession(raw);
|
|
247
|
+
return samePrincipal(node.sessionDid, tenant.sessionDid) && ownerDids.some((did) => samePrincipal(node.did, did));
|
|
248
|
+
} catch {
|
|
249
|
+
await (0, import_promises.rm)(this.#sessionPath(tenant.stateRoot), { force: true });
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async #initializeTenant(tenantId) {
|
|
254
|
+
const stateRoot = (0, import_node_path.join)(this.#config.stateDir, "tenants", tenantId);
|
|
255
|
+
const keyPath = this.#keyPath(stateRoot);
|
|
256
|
+
await secureDirectory((0, import_node_path.join)(stateRoot, ".tinycloud", "profiles", PROFILE));
|
|
257
|
+
let jwk = await readJson(keyPath);
|
|
258
|
+
let sessionDid;
|
|
259
|
+
if (jwk === null) {
|
|
260
|
+
const generated = generateSessionKey();
|
|
261
|
+
jwk = generated.jwk;
|
|
262
|
+
sessionDid = generated.did;
|
|
263
|
+
await secureWrite(keyPath, jwk);
|
|
264
|
+
} else {
|
|
265
|
+
sessionDid = didFromJwk(jwk);
|
|
266
|
+
}
|
|
267
|
+
const profilePath = this.#profilePath(stateRoot);
|
|
268
|
+
if (!await fileExists(profilePath)) {
|
|
269
|
+
await secureWrite(profilePath, {
|
|
270
|
+
name: PROFILE,
|
|
271
|
+
host: this.#config.nodeHost,
|
|
272
|
+
chainId: 1,
|
|
273
|
+
spaceName: "default",
|
|
274
|
+
did: sessionDid,
|
|
275
|
+
sessionDid,
|
|
276
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
277
|
+
posture: "delegate-session",
|
|
278
|
+
operatorType: "agent",
|
|
279
|
+
authMethod: "openkey",
|
|
280
|
+
openkeyHost: this.#config.openkeyHost
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
return { stateRoot, sessionDid, jwk };
|
|
284
|
+
}
|
|
285
|
+
async #readTenant(tenantId) {
|
|
286
|
+
if (!/^[a-f0-9]{64}$/.test(tenantId)) throw new Error("Approval state is invalid.");
|
|
287
|
+
const stateRoot = (0, import_node_path.join)(this.#config.stateDir, "tenants", tenantId);
|
|
288
|
+
const jwk = await readJson(this.#keyPath(stateRoot));
|
|
289
|
+
if (jwk === null) throw new Error("The TinyCloud delegate no longer exists.");
|
|
290
|
+
return { stateRoot, jwk, sessionDid: didFromJwk(jwk) };
|
|
291
|
+
}
|
|
292
|
+
async #createPending(subject, ownerDids, kind, permissions, requestId) {
|
|
293
|
+
const tenant = await this.#ensureTenant(subject);
|
|
294
|
+
const tenantId = this.#tenantId(subject);
|
|
295
|
+
const reusable = await reusablePending(
|
|
296
|
+
this.#pendingDirectory(tenant.stateRoot),
|
|
297
|
+
{ tenantId, kind, requestId, permissions, ownerDids: ownerDids.map(normalizeDid) }
|
|
298
|
+
);
|
|
299
|
+
if (reusable !== void 0) return reusable;
|
|
300
|
+
const nonce = (0, import_node_crypto.randomBytes)(24).toString("base64url");
|
|
301
|
+
const created = Date.now();
|
|
302
|
+
const pending = {
|
|
303
|
+
version: 1,
|
|
304
|
+
nonce,
|
|
305
|
+
tenantId,
|
|
306
|
+
kind,
|
|
307
|
+
...requestId === void 0 ? {} : { requestId },
|
|
308
|
+
permissions,
|
|
309
|
+
ownerDids: ownerDids.map(normalizeDid),
|
|
310
|
+
createdAt: new Date(created).toISOString(),
|
|
311
|
+
expiresAt: new Date(created + this.#config.approvalTtlSeconds * 1e3).toISOString()
|
|
312
|
+
};
|
|
313
|
+
await secureWrite(this.#pendingPath(tenant.stateRoot, nonce), pending);
|
|
314
|
+
return pending;
|
|
315
|
+
}
|
|
316
|
+
async #readPending(state, consume) {
|
|
317
|
+
const [tenantId, nonce, signature] = state.split(".");
|
|
318
|
+
if (tenantId === void 0 || nonce === void 0 || signature === void 0) {
|
|
319
|
+
throw new Error("Approval state is invalid.");
|
|
320
|
+
}
|
|
321
|
+
const expected = this.#signState(tenantId, nonce);
|
|
322
|
+
const supplied = Buffer.from(signature, "base64url");
|
|
323
|
+
const expectedBytes = Buffer.from(expected, "base64url");
|
|
324
|
+
if (supplied.length !== expectedBytes.length || !(0, import_node_crypto.timingSafeEqual)(supplied, expectedBytes)) {
|
|
325
|
+
throw new Error("Approval state is invalid.");
|
|
326
|
+
}
|
|
327
|
+
const stateRoot = (0, import_node_path.join)(this.#config.stateDir, "tenants", tenantId);
|
|
328
|
+
const path = this.#pendingPath(stateRoot, nonce);
|
|
329
|
+
const claimed = `${path}.processing`;
|
|
330
|
+
if (consume) await (0, import_promises.rename)(path, claimed);
|
|
331
|
+
try {
|
|
332
|
+
const pending = await readJson(consume ? claimed : path);
|
|
333
|
+
if (pending === null || pending.version !== 1 || pending.tenantId !== tenantId || pending.nonce !== nonce) {
|
|
334
|
+
throw new Error("Approval state is invalid or already used.");
|
|
335
|
+
}
|
|
336
|
+
if (new Date(pending.expiresAt).getTime() <= Date.now()) {
|
|
337
|
+
await (0, import_promises.rm)(consume ? claimed : path, { force: true });
|
|
338
|
+
throw new Error("Approval state has expired.");
|
|
339
|
+
}
|
|
340
|
+
return pending;
|
|
341
|
+
} catch (error) {
|
|
342
|
+
if (consume) await (0, import_promises.rename)(claimed, path).catch(() => void 0);
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async #restorePending(pending) {
|
|
347
|
+
const stateRoot = (0, import_node_path.join)(this.#config.stateDir, "tenants", pending.tenantId);
|
|
348
|
+
const path = this.#pendingPath(stateRoot, pending.nonce);
|
|
349
|
+
await (0, import_promises.rename)(`${path}.processing`, path);
|
|
350
|
+
}
|
|
351
|
+
#connectUrl(pending) {
|
|
352
|
+
const url = new URL("/connect", this.#config.publicUrl);
|
|
353
|
+
url.searchParams.set("state", `${pending.tenantId}.${pending.nonce}.${this.#signState(pending.tenantId, pending.nonce)}`);
|
|
354
|
+
return url.toString();
|
|
355
|
+
}
|
|
356
|
+
#tenantId(subject) {
|
|
357
|
+
return (0, import_node_crypto.createHmac)("sha256", this.#config.stateSecret).update(`tenant:${subject}`).digest("hex");
|
|
358
|
+
}
|
|
359
|
+
#signState(tenantId, nonce) {
|
|
360
|
+
return (0, import_node_crypto.createHmac)("sha256", this.#config.stateSecret).update(`approval:${tenantId}:${nonce}`).digest("base64url");
|
|
361
|
+
}
|
|
362
|
+
#profilePath(stateRoot) {
|
|
363
|
+
return (0, import_node_path.join)(stateRoot, ".tinycloud", "profiles", PROFILE, "profile.json");
|
|
364
|
+
}
|
|
365
|
+
#keyPath(stateRoot) {
|
|
366
|
+
return (0, import_node_path.join)(stateRoot, ".tinycloud", "profiles", PROFILE, "key.json");
|
|
367
|
+
}
|
|
368
|
+
#sessionPath(stateRoot) {
|
|
369
|
+
return (0, import_node_path.join)(stateRoot, ".tinycloud", "profiles", PROFILE, "session.json");
|
|
370
|
+
}
|
|
371
|
+
#pendingPath(stateRoot, nonce) {
|
|
372
|
+
return (0, import_node_path.join)(this.#pendingDirectory(stateRoot), `${nonce}.json`);
|
|
373
|
+
}
|
|
374
|
+
#pendingDirectory(stateRoot) {
|
|
375
|
+
return (0, import_node_path.join)(stateRoot, ".tinycloud", "profiles", PROFILE, "pending");
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
var wasmInitialized = false;
|
|
379
|
+
function generateSessionKey() {
|
|
380
|
+
ensureWasm();
|
|
381
|
+
const manager = new import_node_sdk_wasm.TCWSessionManager();
|
|
382
|
+
const keyId = manager.createSessionKey("hosted-mcp");
|
|
383
|
+
const serialized = manager.jwk(keyId);
|
|
384
|
+
if (!serialized) throw new Error("Could not generate the hosted delegate key.");
|
|
385
|
+
return { jwk: JSON.parse(serialized), did: manager.getDID(keyId) };
|
|
386
|
+
}
|
|
387
|
+
function didFromJwk(jwk) {
|
|
388
|
+
ensureWasm();
|
|
389
|
+
const manager = new import_node_sdk_wasm.TCWSessionManager();
|
|
390
|
+
const keyId = (0, import_node_sdk_wasm.importKey)(manager, JSON.stringify(jwk), "hosted-mcp");
|
|
391
|
+
return manager.getDID(keyId);
|
|
392
|
+
}
|
|
393
|
+
function ensureWasm() {
|
|
394
|
+
if (wasmInitialized) return;
|
|
395
|
+
(0, import_node_sdk_wasm.initPanicHook)();
|
|
396
|
+
wasmInitialized = true;
|
|
397
|
+
}
|
|
398
|
+
function sessionFromCallback(data, privateJwk) {
|
|
399
|
+
const delegationHeader = data.delegationHeader;
|
|
400
|
+
if (!isRecord(delegationHeader) || typeof delegationHeader.Authorization !== "string" || typeof data.delegationCid !== "string" || typeof data.spaceId !== "string" || typeof data.verificationMethod !== "string") {
|
|
401
|
+
throw new Error("OpenKey returned an incomplete delegation.");
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
delegationHeader: { Authorization: delegationHeader.Authorization },
|
|
405
|
+
delegationCid: data.delegationCid,
|
|
406
|
+
spaceId: data.spaceId,
|
|
407
|
+
jwk: privateJwk,
|
|
408
|
+
verificationMethod: data.verificationMethod,
|
|
409
|
+
...typeof data.address === "string" ? { address: data.address } : {},
|
|
410
|
+
...typeof data.chainId === "number" ? { chainId: data.chainId } : {},
|
|
411
|
+
...typeof data.siwe === "string" ? { siwe: data.siwe } : {},
|
|
412
|
+
...typeof data.signature === "string" ? { signature: data.signature } : {}
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
function portableFromCallback(data, permissions, host) {
|
|
416
|
+
const primary = permissions.find((permission) => permission.service !== "tinycloud.encryption") ?? permissions[0];
|
|
417
|
+
if (primary === void 0) throw new Error("The authority request has no permissions.");
|
|
418
|
+
const session = sessionFromCallback(data, {});
|
|
419
|
+
const expiry = delegationExpiry(data);
|
|
420
|
+
return {
|
|
421
|
+
cid: session.delegationCid,
|
|
422
|
+
delegationHeader: session.delegationHeader,
|
|
423
|
+
spaceId: session.spaceId,
|
|
424
|
+
path: primary.path,
|
|
425
|
+
actions: [...primary.actions],
|
|
426
|
+
resources: permissions.map((permission) => ({
|
|
427
|
+
service: permission.service.startsWith("tinycloud.") ? permission.service.slice("tinycloud.".length) : permission.service,
|
|
428
|
+
space: permission.service === "tinycloud.encryption" ? requiredPermissionSpace(permission) : session.spaceId,
|
|
429
|
+
path: permission.path,
|
|
430
|
+
actions: [...permission.actions]
|
|
431
|
+
})),
|
|
432
|
+
expiry,
|
|
433
|
+
delegateDID: session.verificationMethod,
|
|
434
|
+
ownerAddress: typeof data.address === "string" ? data.address : "",
|
|
435
|
+
chainId: typeof data.chainId === "number" ? data.chainId : 1,
|
|
436
|
+
host
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
function delegationExpiry(data) {
|
|
440
|
+
for (const key of ["expiry", "expiresAt", "expirationTime"]) {
|
|
441
|
+
const parsed = parseDate(data[key]);
|
|
442
|
+
if (parsed !== void 0) return parsed;
|
|
443
|
+
}
|
|
444
|
+
if (typeof data.siwe === "string") {
|
|
445
|
+
const match = data.siwe.match(/^Expiration Time:\s*(.+)$/im);
|
|
446
|
+
const parsed = parseDate(match?.[1]?.trim());
|
|
447
|
+
if (parsed !== void 0) return parsed;
|
|
448
|
+
}
|
|
449
|
+
throw new Error("OpenKey did not return a delegation expiry.");
|
|
450
|
+
}
|
|
451
|
+
function permissionsCover(requested, granted) {
|
|
452
|
+
return requested.every((request) => granted.some(
|
|
453
|
+
(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))
|
|
454
|
+
));
|
|
455
|
+
}
|
|
456
|
+
function sameSpace(granted, requested) {
|
|
457
|
+
return granted === requested || granted.endsWith(`:${requested}`);
|
|
458
|
+
}
|
|
459
|
+
function normalizeService(service) {
|
|
460
|
+
return service.startsWith("tinycloud.") ? service : `tinycloud.${service}`;
|
|
461
|
+
}
|
|
462
|
+
function requiredPermissionSpace(permission) {
|
|
463
|
+
if (typeof permission.space !== "string") throw new Error("The authority request has no space.");
|
|
464
|
+
return permission.space;
|
|
465
|
+
}
|
|
466
|
+
function publicJwk(jwk) {
|
|
467
|
+
const copy = { ...jwk };
|
|
468
|
+
for (const field of ["d", "p", "q", "dp", "dq", "qi", "oth", "k"]) delete copy[field];
|
|
469
|
+
return copy;
|
|
470
|
+
}
|
|
471
|
+
function samePrincipal(left, right) {
|
|
472
|
+
if (typeof left !== "string") return false;
|
|
473
|
+
try {
|
|
474
|
+
return (0, import_node_sdk.principalDidEquals)(left, right);
|
|
475
|
+
} catch {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
function normalizeDid(value) {
|
|
480
|
+
return value.split("#", 1)[0];
|
|
481
|
+
}
|
|
482
|
+
function isPermissionEntry(value) {
|
|
483
|
+
return isRecord(value) && typeof value.service === "string" && typeof value.space === "string" && typeof value.path === "string" && Array.isArray(value.actions) && value.actions.every((action) => typeof action === "string");
|
|
484
|
+
}
|
|
485
|
+
function isRecord(value) {
|
|
486
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
487
|
+
}
|
|
488
|
+
function parseDate(value) {
|
|
489
|
+
if (typeof value !== "string" && typeof value !== "number") return void 0;
|
|
490
|
+
const date = new Date(typeof value === "number" && value < 1e10 ? value * 1e3 : value);
|
|
491
|
+
return Number.isNaN(date.getTime()) ? void 0 : date;
|
|
492
|
+
}
|
|
493
|
+
async function secureDirectory(path) {
|
|
494
|
+
await (0, import_promises.mkdir)(path, { recursive: true, mode: 448 });
|
|
495
|
+
await (0, import_promises.chmod)(path, 448);
|
|
496
|
+
}
|
|
497
|
+
async function secureWrite(path, value) {
|
|
498
|
+
const directory = (0, import_node_path.dirname)(path);
|
|
499
|
+
await secureDirectory(directory);
|
|
500
|
+
const temporary = `${path}.${process.pid}.${(0, import_node_crypto.randomBytes)(8).toString("hex")}.tmp`;
|
|
501
|
+
await (0, import_promises.writeFile)(temporary, `${JSON.stringify(value, jsonReplacer, 2)}
|
|
502
|
+
`, { mode: 384 });
|
|
503
|
+
await (0, import_promises.rename)(temporary, path);
|
|
504
|
+
await (0, import_promises.chmod)(path, 384);
|
|
505
|
+
}
|
|
506
|
+
function jsonReplacer(_key, value) {
|
|
507
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
508
|
+
}
|
|
509
|
+
async function readJson(path) {
|
|
510
|
+
try {
|
|
511
|
+
return JSON.parse(await (0, import_promises.readFile)(path, "utf8"));
|
|
512
|
+
} catch (error) {
|
|
513
|
+
if (isRecord(error) && error.code === "ENOENT") return null;
|
|
514
|
+
throw error;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
async function fileExists(path) {
|
|
518
|
+
return await readJson(path) !== null;
|
|
519
|
+
}
|
|
520
|
+
async function reusablePending(directory, expected) {
|
|
521
|
+
let names;
|
|
522
|
+
try {
|
|
523
|
+
names = await (0, import_promises.readdir)(directory);
|
|
524
|
+
} catch (error) {
|
|
525
|
+
if (isRecord(error) && error.code === "ENOENT") return void 0;
|
|
526
|
+
throw error;
|
|
527
|
+
}
|
|
528
|
+
const active = [];
|
|
529
|
+
for (const name of names) {
|
|
530
|
+
if (!name.endsWith(".json")) continue;
|
|
531
|
+
const path = (0, import_node_path.join)(directory, name);
|
|
532
|
+
const pending = await readJson(path).catch(() => null);
|
|
533
|
+
if (pending === null || new Date(pending.expiresAt).getTime() <= Date.now()) {
|
|
534
|
+
await (0, import_promises.rm)(path, { force: true });
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
active.push(pending);
|
|
538
|
+
if (samePendingRequest(pending, expected)) return pending;
|
|
539
|
+
}
|
|
540
|
+
if (active.length >= 64) {
|
|
541
|
+
active.sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt));
|
|
542
|
+
await (0, import_promises.rm)((0, import_node_path.join)(directory, `${active[0].nonce}.json`), { force: true });
|
|
543
|
+
}
|
|
544
|
+
return void 0;
|
|
545
|
+
}
|
|
546
|
+
function samePendingRequest(pending, expected) {
|
|
547
|
+
return pending.version === 1 && pending.tenantId === expected.tenantId && pending.kind === expected.kind && pending.requestId === expected.requestId && JSON.stringify(pending.permissions) === JSON.stringify(expected.permissions) && JSON.stringify(pending.ownerDids) === JSON.stringify(expected.ownerDids);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/server.ts
|
|
551
|
+
var import_server3 = require("@modelcontextprotocol/server");
|
|
552
|
+
var import_stdio = require("@modelcontextprotocol/server/stdio");
|
|
553
|
+
|
|
554
|
+
// src/tools.ts
|
|
555
|
+
var import_node_module = require("module");
|
|
556
|
+
var import_node_path2 = require("path");
|
|
557
|
+
var import_server2 = require("@modelcontextprotocol/server");
|
|
558
|
+
var import_ajv = require("@modelcontextprotocol/server/validators/ajv");
|
|
559
|
+
var import_operations2 = require("@tinycloud/operations");
|
|
560
|
+
|
|
561
|
+
// src/results.ts
|
|
562
|
+
function toMcpToolResult(result) {
|
|
563
|
+
const canonical = isCanonicalOperationResult(result) ? result : {
|
|
564
|
+
status: "error",
|
|
565
|
+
operation: {},
|
|
566
|
+
context: {},
|
|
567
|
+
error: { code: "INTERNAL_ERROR" }
|
|
568
|
+
};
|
|
569
|
+
return {
|
|
570
|
+
content: [{ type: "text", text: resultSummary(canonical.status) }],
|
|
571
|
+
structuredContent: canonical
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
function isCanonicalOperationResult(value) {
|
|
575
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
576
|
+
const status = value.status;
|
|
577
|
+
return status === "ok" || status === "authority_required" || status === "setup_required" || status === "error";
|
|
578
|
+
}
|
|
579
|
+
function resultSummary(status) {
|
|
580
|
+
switch (status) {
|
|
581
|
+
case "ok":
|
|
582
|
+
return "TinyCloud operation completed; use the structured result.";
|
|
583
|
+
case "authority_required":
|
|
584
|
+
return "TinyCloud authority is required; use the structured request and approval action.";
|
|
585
|
+
case "setup_required":
|
|
586
|
+
return "TinyCloud setup is required; use the structured setup action.";
|
|
587
|
+
case "error":
|
|
588
|
+
return "TinyCloud operation failed; use the structured error.";
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// src/tools.ts
|
|
593
|
+
var import_meta = {};
|
|
594
|
+
function invocationTargetForMcp(selection) {
|
|
595
|
+
return {
|
|
596
|
+
profile: selection.profile,
|
|
597
|
+
...selection.explicitProfile && selection.allowOwnerProfile ? { allowOwnerProfile: true } : {},
|
|
598
|
+
...selection.stateRoot === void 0 ? {} : { stateRoot: selection.stateRoot }
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
var packageRequire = (0, import_node_module.createRequire)(
|
|
602
|
+
typeof __dirname === "string" ? (0, import_node_path2.join)(__dirname, "tools.cjs") : import_meta.url
|
|
603
|
+
);
|
|
604
|
+
var generatedCatalog = packageRequire(
|
|
605
|
+
"@tinycloud/operations/operations.json"
|
|
606
|
+
);
|
|
607
|
+
var TOOL_BINDINGS = [
|
|
608
|
+
{
|
|
609
|
+
name: "tinycloud_status",
|
|
610
|
+
operationId: "tinycloud.status.get",
|
|
611
|
+
operationVersion: 1,
|
|
612
|
+
readOnlyHint: true,
|
|
613
|
+
idempotentHint: true,
|
|
614
|
+
destructiveHint: false,
|
|
615
|
+
openWorldHint: false
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
name: "tinycloud_auth_status",
|
|
619
|
+
operationId: "tinycloud.auth.status",
|
|
620
|
+
operationVersion: 1,
|
|
621
|
+
readOnlyHint: true,
|
|
622
|
+
idempotentHint: true,
|
|
623
|
+
destructiveHint: false,
|
|
624
|
+
openWorldHint: false
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
name: "tinycloud_auth_capabilities",
|
|
628
|
+
operationId: "tinycloud.auth.capabilities",
|
|
629
|
+
operationVersion: 1,
|
|
630
|
+
readOnlyHint: true,
|
|
631
|
+
idempotentHint: true,
|
|
632
|
+
destructiveHint: false,
|
|
633
|
+
openWorldHint: false
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
name: "tinycloud_auth_request",
|
|
637
|
+
operationId: "tinycloud.auth.request",
|
|
638
|
+
operationVersion: 1,
|
|
639
|
+
readOnlyHint: false,
|
|
640
|
+
idempotentHint: true,
|
|
641
|
+
destructiveHint: false,
|
|
642
|
+
openWorldHint: false
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
name: "tinycloud_auth_import",
|
|
646
|
+
operationId: "tinycloud.auth.import",
|
|
647
|
+
operationVersion: 1,
|
|
648
|
+
readOnlyHint: false,
|
|
649
|
+
idempotentHint: true,
|
|
650
|
+
destructiveHint: false,
|
|
651
|
+
openWorldHint: false
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
name: "tinycloud_account_spaces_list",
|
|
655
|
+
operationId: "tinycloud.account.spaces.list",
|
|
656
|
+
operationVersion: 1,
|
|
657
|
+
readOnlyHint: true,
|
|
658
|
+
idempotentHint: true,
|
|
659
|
+
destructiveHint: false,
|
|
660
|
+
openWorldHint: true
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
name: "tinycloud_account_applications_list",
|
|
664
|
+
operationId: "tinycloud.account.applications.list",
|
|
665
|
+
operationVersion: 1,
|
|
666
|
+
readOnlyHint: true,
|
|
667
|
+
idempotentHint: true,
|
|
668
|
+
destructiveHint: false,
|
|
669
|
+
openWorldHint: true
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
name: "tinycloud_kv_list",
|
|
673
|
+
operationId: "tinycloud.kv.list",
|
|
674
|
+
operationVersion: 1,
|
|
675
|
+
readOnlyHint: true,
|
|
676
|
+
idempotentHint: true,
|
|
677
|
+
destructiveHint: false,
|
|
678
|
+
openWorldHint: true
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
name: "tinycloud_kv_get",
|
|
682
|
+
operationId: "tinycloud.kv.get",
|
|
683
|
+
operationVersion: 1,
|
|
684
|
+
readOnlyHint: true,
|
|
685
|
+
idempotentHint: true,
|
|
686
|
+
destructiveHint: false,
|
|
687
|
+
openWorldHint: true
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
name: "tinycloud_kv_head",
|
|
691
|
+
operationId: "tinycloud.kv.head",
|
|
692
|
+
operationVersion: 1,
|
|
693
|
+
readOnlyHint: true,
|
|
694
|
+
idempotentHint: true,
|
|
695
|
+
destructiveHint: false,
|
|
696
|
+
openWorldHint: true
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
name: "tinycloud_kv_put",
|
|
700
|
+
operationId: "tinycloud.kv.put",
|
|
701
|
+
operationVersion: 1,
|
|
702
|
+
readOnlyHint: false,
|
|
703
|
+
idempotentHint: true,
|
|
704
|
+
destructiveHint: true,
|
|
705
|
+
openWorldHint: true
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
name: "tinycloud_kv_delete",
|
|
709
|
+
operationId: "tinycloud.kv.delete",
|
|
710
|
+
operationVersion: 1,
|
|
711
|
+
readOnlyHint: false,
|
|
712
|
+
idempotentHint: true,
|
|
713
|
+
destructiveHint: true,
|
|
714
|
+
openWorldHint: true
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
name: "tinycloud_sql_schema_inspect",
|
|
718
|
+
operationId: "tinycloud.sql.schema.inspect",
|
|
719
|
+
operationVersion: 1,
|
|
720
|
+
readOnlyHint: true,
|
|
721
|
+
idempotentHint: true,
|
|
722
|
+
destructiveHint: false,
|
|
723
|
+
openWorldHint: true
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
name: "tinycloud_sql_query",
|
|
727
|
+
operationId: "tinycloud.sql.query",
|
|
728
|
+
operationVersion: 1,
|
|
729
|
+
readOnlyHint: true,
|
|
730
|
+
idempotentHint: true,
|
|
731
|
+
destructiveHint: false,
|
|
732
|
+
openWorldHint: true
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
name: "tinycloud_sql_execute",
|
|
736
|
+
operationId: "tinycloud.sql.execute",
|
|
737
|
+
operationVersion: 1,
|
|
738
|
+
readOnlyHint: false,
|
|
739
|
+
idempotentHint: false,
|
|
740
|
+
destructiveHint: true,
|
|
741
|
+
openWorldHint: true
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
name: "tinycloud_secrets_get",
|
|
745
|
+
operationId: "tinycloud.secrets.get",
|
|
746
|
+
operationVersion: 1,
|
|
747
|
+
readOnlyHint: false,
|
|
748
|
+
idempotentHint: true,
|
|
749
|
+
destructiveHint: false,
|
|
750
|
+
openWorldHint: true
|
|
751
|
+
}
|
|
752
|
+
];
|
|
753
|
+
function createJsonSchemaValidator() {
|
|
754
|
+
const ajv = new import_ajv.Ajv({ strict: false });
|
|
755
|
+
(0, import_ajv.addFormats)(ajv);
|
|
756
|
+
return new import_ajv.AjvJsonSchemaValidator(ajv);
|
|
757
|
+
}
|
|
758
|
+
function registerTinyCloudTools(server, selection, validator) {
|
|
759
|
+
const definitions = new Map(
|
|
760
|
+
generatedCatalog.operations.map((operation) => [
|
|
761
|
+
`${operation.id}@${operation.version}`,
|
|
762
|
+
operation
|
|
763
|
+
])
|
|
764
|
+
);
|
|
765
|
+
for (const binding of TOOL_BINDINGS) {
|
|
766
|
+
const operation = definitions.get(`${binding.operationId}@${binding.operationVersion}`);
|
|
767
|
+
if (operation === void 0) {
|
|
768
|
+
throw new Error(`Generated operations catalog is missing ${binding.operationId}@${binding.operationVersion}.`);
|
|
769
|
+
}
|
|
770
|
+
const inputSchema = (0, import_server2.fromJsonSchema)(
|
|
771
|
+
operation.input,
|
|
772
|
+
validator
|
|
773
|
+
);
|
|
774
|
+
const outputSchema = (0, import_server2.fromJsonSchema)(
|
|
775
|
+
operation.result,
|
|
776
|
+
validator
|
|
777
|
+
);
|
|
778
|
+
const annotations = {
|
|
779
|
+
readOnlyHint: binding.readOnlyHint,
|
|
780
|
+
idempotentHint: binding.idempotentHint,
|
|
781
|
+
destructiveHint: binding.destructiveHint,
|
|
782
|
+
openWorldHint: binding.openWorldHint
|
|
783
|
+
};
|
|
784
|
+
server.registerTool(
|
|
785
|
+
binding.name,
|
|
786
|
+
{
|
|
787
|
+
title: operation.title,
|
|
788
|
+
description: operation.description,
|
|
789
|
+
inputSchema,
|
|
790
|
+
outputSchema,
|
|
791
|
+
annotations
|
|
792
|
+
},
|
|
793
|
+
async (input) => {
|
|
794
|
+
const result = await (0, import_operations2.invokeOperation)(
|
|
795
|
+
binding.operationId,
|
|
796
|
+
binding.operationVersion,
|
|
797
|
+
invocationTargetForMcp(selection),
|
|
798
|
+
input
|
|
799
|
+
);
|
|
800
|
+
const projected = selection.transformOperationResult === void 0 ? result : await selection.transformOperationResult(result);
|
|
801
|
+
return toMcpToolResult(projected);
|
|
802
|
+
}
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// package.json
|
|
808
|
+
var package_default = {
|
|
809
|
+
name: "@tinycloud/mcp",
|
|
810
|
+
version: "0.3.0-beta.0",
|
|
811
|
+
description: "TinyCloud delegated operations MCP server",
|
|
812
|
+
keywords: [
|
|
813
|
+
"tinycloud",
|
|
814
|
+
"mcp",
|
|
815
|
+
"model-context-protocol",
|
|
816
|
+
"agents",
|
|
817
|
+
"delegation",
|
|
818
|
+
"ucan",
|
|
819
|
+
"kv",
|
|
820
|
+
"sqlite"
|
|
821
|
+
],
|
|
822
|
+
author: "TinyCloud, Inc.",
|
|
823
|
+
license: "EGPL",
|
|
824
|
+
type: "module",
|
|
825
|
+
main: "dist/index.cjs",
|
|
826
|
+
module: "dist/index.js",
|
|
827
|
+
types: "dist/index.d.ts",
|
|
828
|
+
bin: {
|
|
829
|
+
"tinycloud-mcp": "./bin/tinycloud-mcp",
|
|
830
|
+
"tinycloud-mcp-http": "./bin/tinycloud-mcp-http"
|
|
831
|
+
},
|
|
832
|
+
exports: {
|
|
833
|
+
".": {
|
|
834
|
+
types: "./dist/index.d.ts",
|
|
835
|
+
import: "./dist/index.js",
|
|
836
|
+
require: "./dist/index.cjs"
|
|
837
|
+
},
|
|
838
|
+
"./server": {
|
|
839
|
+
types: "./dist/server.d.ts",
|
|
840
|
+
import: "./dist/server.js",
|
|
841
|
+
require: "./dist/server.cjs"
|
|
842
|
+
},
|
|
843
|
+
"./tools": {
|
|
844
|
+
types: "./dist/tools.d.ts",
|
|
845
|
+
import: "./dist/tools.js",
|
|
846
|
+
require: "./dist/tools.cjs"
|
|
847
|
+
},
|
|
848
|
+
"./results": {
|
|
849
|
+
types: "./dist/results.d.ts",
|
|
850
|
+
import: "./dist/results.js",
|
|
851
|
+
require: "./dist/results.cjs"
|
|
852
|
+
},
|
|
853
|
+
"./http": {
|
|
854
|
+
types: "./dist/http.d.ts",
|
|
855
|
+
import: "./dist/http.js",
|
|
856
|
+
require: "./dist/http.cjs"
|
|
857
|
+
}
|
|
858
|
+
},
|
|
859
|
+
files: [
|
|
860
|
+
"dist",
|
|
861
|
+
"bin",
|
|
862
|
+
"generated",
|
|
863
|
+
"skills"
|
|
864
|
+
],
|
|
865
|
+
engines: {
|
|
866
|
+
node: ">=20"
|
|
867
|
+
},
|
|
868
|
+
scripts: {
|
|
869
|
+
build: "tsup",
|
|
870
|
+
clean: "rm -rf dist",
|
|
871
|
+
test: "bun run build && bun scripts/test.ts && bun run check:generated",
|
|
872
|
+
"test:stdio": "bun run build && bun test src/stdio.test.ts",
|
|
873
|
+
typecheck: "tsc -p tsconfig.json",
|
|
874
|
+
"start:http": "node dist/http-cli.js",
|
|
875
|
+
generate: "bun scripts/generate.ts",
|
|
876
|
+
"check:generated": "bun scripts/generate.ts --check"
|
|
877
|
+
},
|
|
878
|
+
dependencies: {
|
|
879
|
+
"@modelcontextprotocol/node": "2.0.0-beta.4",
|
|
880
|
+
"@modelcontextprotocol/server": "2.0.0-beta.4",
|
|
881
|
+
"@tinycloud/node-sdk": "2.9.0",
|
|
882
|
+
"@tinycloud/node-sdk-wasm": "1.7.5",
|
|
883
|
+
"@tinycloud/operations": "0.3.0-beta.0",
|
|
884
|
+
jose: "6.1.3"
|
|
885
|
+
},
|
|
886
|
+
devDependencies: {
|
|
887
|
+
"@modelcontextprotocol/client": "2.0.0-beta.4",
|
|
888
|
+
"@types/node": "^20",
|
|
889
|
+
tsup: "^8.0.0",
|
|
890
|
+
typescript: "^5.9.3"
|
|
891
|
+
},
|
|
892
|
+
repository: {
|
|
893
|
+
type: "git",
|
|
894
|
+
url: "https://github.com/TinyCloudLabs/js-sdk"
|
|
895
|
+
},
|
|
896
|
+
homepage: "https://docs.tinycloud.xyz/cli/mcp"
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
// src/version.ts
|
|
900
|
+
var MCP_VERSION = package_default.version;
|
|
901
|
+
|
|
902
|
+
// src/server.ts
|
|
903
|
+
var MCP_SERVER_NAME = "tinycloud-mcp";
|
|
904
|
+
|
|
905
|
+
// src/http.ts
|
|
906
|
+
var REQUIRED_SCOPE = "tinycloud:mcp";
|
|
907
|
+
function createHostedMcpApp(config) {
|
|
908
|
+
const resourceUrl = canonicalResourceUrl(config.publicUrl);
|
|
909
|
+
const issuer = String(config.oauthMetadata.issuer);
|
|
910
|
+
const jwksUri = String(config.oauthMetadata.jwks_uri);
|
|
911
|
+
const verifier = createOpenKeyTokenVerifier({
|
|
912
|
+
issuer,
|
|
913
|
+
audience: resourceUrl.toString(),
|
|
914
|
+
jwksUri,
|
|
915
|
+
requiredScope: REQUIRED_SCOPE
|
|
916
|
+
});
|
|
917
|
+
const resourceMetadataUrl = (0, import_server4.getOAuthProtectedResourceMetadataUrl)(resourceUrl);
|
|
918
|
+
const authenticate = (0, import_server4.requireBearerAuth)({
|
|
919
|
+
verifier,
|
|
920
|
+
requiredScopes: [REQUIRED_SCOPE],
|
|
921
|
+
resourceMetadataUrl
|
|
922
|
+
});
|
|
923
|
+
const store = new RemoteTenantStore({ ...config, publicUrl: resourceUrl });
|
|
924
|
+
const handler = (0, import_server4.createMcpHandler)(async ({ authInfo: authInfo2 }) => {
|
|
925
|
+
const identity = oauthIdentity(authInfo2);
|
|
926
|
+
const { subject, ownerDids } = identity;
|
|
927
|
+
const stateRoot = store.tenantStateRoot(subject);
|
|
928
|
+
const validator = createJsonSchemaValidator();
|
|
929
|
+
const server = new import_server4.McpServer(
|
|
930
|
+
{ name: MCP_SERVER_NAME, version: MCP_VERSION },
|
|
931
|
+
{
|
|
932
|
+
jsonSchemaValidator: validator,
|
|
933
|
+
instructions: "Call tinycloud_connect first. If an operation returns authority_required, open approval.url, then retry that operation unchanged."
|
|
934
|
+
}
|
|
935
|
+
);
|
|
936
|
+
registerTinyCloudTools(server, {
|
|
937
|
+
profile: "agent",
|
|
938
|
+
explicitProfile: true,
|
|
939
|
+
allowOwnerProfile: false,
|
|
940
|
+
stateRoot,
|
|
941
|
+
transformOperationResult: (result) => store.decorateAuthorityResult(subject, ownerDids, result)
|
|
942
|
+
}, validator);
|
|
943
|
+
registerConnectTool(server, validator, store, identity);
|
|
944
|
+
return server;
|
|
945
|
+
}, {
|
|
946
|
+
legacy: "stateless",
|
|
947
|
+
onerror: () => writeDiagnostic("MCP request failed")
|
|
948
|
+
});
|
|
949
|
+
const metadataOptions = {
|
|
950
|
+
oauthMetadata: config.oauthMetadata,
|
|
951
|
+
resourceServerUrl: resourceUrl,
|
|
952
|
+
serviceDocumentationUrl: new URL("https://docs.tinycloud.xyz/cli/mcp"),
|
|
953
|
+
scopesSupported: [REQUIRED_SCOPE],
|
|
954
|
+
resourceName: "TinyCloud hosted MCP"
|
|
955
|
+
};
|
|
956
|
+
const allowedHosts = [resourceUrl.hostname];
|
|
957
|
+
return {
|
|
958
|
+
async fetch(request) {
|
|
959
|
+
const metadata = (0, import_server4.oauthMetadataResponse)(request, metadataOptions);
|
|
960
|
+
if (metadata !== void 0) return metadata;
|
|
961
|
+
const url = new URL(request.url);
|
|
962
|
+
if (url.pathname === "/healthz") {
|
|
963
|
+
return request.method === "GET" ? json({ ok: true }) : methodNotAllowed("GET");
|
|
964
|
+
}
|
|
965
|
+
const hostFailure = (0, import_server4.hostHeaderValidationResponse)(request, allowedHosts);
|
|
966
|
+
if (hostFailure !== void 0) return hostFailure;
|
|
967
|
+
if (url.pathname === "/connect") {
|
|
968
|
+
if (request.method !== "GET") return methodNotAllowed("GET");
|
|
969
|
+
try {
|
|
970
|
+
const destination = await store.approvalRedirect(requiredState(url));
|
|
971
|
+
return Response.redirect(destination, 302);
|
|
972
|
+
} catch {
|
|
973
|
+
return json({ error: "The approval link is invalid or expired." }, 400);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
if (url.pathname === "/connect/callback") {
|
|
977
|
+
if (request.method === "OPTIONS") return callbackCors(new Response(null, { status: 204 }), config.openkeyHost);
|
|
978
|
+
if (request.method !== "POST") return methodNotAllowed("POST");
|
|
979
|
+
const originFailure2 = validateCallbackOrigin(request, config.openkeyHost);
|
|
980
|
+
if (originFailure2 !== void 0) return callbackCors(originFailure2, config.openkeyHost);
|
|
981
|
+
try {
|
|
982
|
+
await store.completeApproval(requiredState(url), request);
|
|
983
|
+
return callbackCors(json({ success: true }), config.openkeyHost);
|
|
984
|
+
} catch {
|
|
985
|
+
return callbackCors(json({ error: "TinyCloud could not accept this delegation." }, 400), config.openkeyHost);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
if (url.pathname !== resourceUrl.pathname) return json({ error: "Not found" }, 404);
|
|
989
|
+
const originFailure = validateOrigin(request, config.allowedOrigins);
|
|
990
|
+
if (originFailure !== void 0) return originFailure;
|
|
991
|
+
const auth = await authenticate(request);
|
|
992
|
+
if (auth instanceof Response) return auth;
|
|
993
|
+
return handler.fetch(request, { authInfo: auth });
|
|
994
|
+
},
|
|
995
|
+
close: () => handler.close()
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
function registerConnectTool(server, validator, store, identity) {
|
|
999
|
+
const inputSchema = (0, import_server4.fromJsonSchema)({ type: "object", properties: {}, additionalProperties: false }, validator);
|
|
1000
|
+
const outputSchema = (0, import_server4.fromJsonSchema)({
|
|
1001
|
+
type: "object",
|
|
1002
|
+
properties: {
|
|
1003
|
+
connected: { type: "boolean" },
|
|
1004
|
+
sessionDid: { type: "string" },
|
|
1005
|
+
approvalUrl: { type: "string", format: "uri" },
|
|
1006
|
+
expiresAt: { type: "string", format: "date-time" }
|
|
1007
|
+
},
|
|
1008
|
+
required: ["connected", "sessionDid"],
|
|
1009
|
+
additionalProperties: false
|
|
1010
|
+
}, validator);
|
|
1011
|
+
server.registerTool(
|
|
1012
|
+
"tinycloud_connect",
|
|
1013
|
+
{
|
|
1014
|
+
title: "Connect TinyCloud",
|
|
1015
|
+
description: "Check the hosted delegate connection and return a one-time OpenKey approval URL when setup is required.",
|
|
1016
|
+
inputSchema,
|
|
1017
|
+
outputSchema,
|
|
1018
|
+
annotations: {
|
|
1019
|
+
readOnlyHint: false,
|
|
1020
|
+
idempotentHint: true,
|
|
1021
|
+
destructiveHint: false,
|
|
1022
|
+
openWorldHint: true
|
|
1023
|
+
}
|
|
1024
|
+
},
|
|
1025
|
+
async () => {
|
|
1026
|
+
const status = await store.connectStatus(identity.subject, identity.ownerDids);
|
|
1027
|
+
return {
|
|
1028
|
+
content: [{
|
|
1029
|
+
type: "text",
|
|
1030
|
+
text: status.connected ? "TinyCloud is connected." : "TinyCloud approval is required; open approvalUrl, then call this tool again."
|
|
1031
|
+
}],
|
|
1032
|
+
structuredContent: status
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
function oauthIdentity(authInfo2) {
|
|
1038
|
+
const subject = authInfo2?.extra?.subject;
|
|
1039
|
+
if (typeof subject !== "string" || subject.length === 0) {
|
|
1040
|
+
throw new Error("The authenticated MCP request has no OpenKey subject.");
|
|
1041
|
+
}
|
|
1042
|
+
const ownerDids = authInfo2?.extra?.ownerDids;
|
|
1043
|
+
if (!Array.isArray(ownerDids) || ownerDids.length === 0 || !ownerDids.every((did) => typeof did === "string")) {
|
|
1044
|
+
throw new Error("The authenticated MCP request has no OpenKey owner identity.");
|
|
1045
|
+
}
|
|
1046
|
+
return { subject, ownerDids };
|
|
1047
|
+
}
|
|
1048
|
+
function canonicalResourceUrl(url) {
|
|
1049
|
+
const resource = new URL(url);
|
|
1050
|
+
resource.hash = "";
|
|
1051
|
+
resource.search = "";
|
|
1052
|
+
resource.pathname = resource.pathname.replace(/\/+$/, "") || "/mcp";
|
|
1053
|
+
if (resource.pathname === "/") resource.pathname = "/mcp";
|
|
1054
|
+
return resource;
|
|
1055
|
+
}
|
|
1056
|
+
function requiredState(url) {
|
|
1057
|
+
const state = url.searchParams.get("state");
|
|
1058
|
+
if (state === null || state.length > 512) throw new Error("Missing approval state.");
|
|
1059
|
+
return state;
|
|
1060
|
+
}
|
|
1061
|
+
function validateOrigin(request, allowed) {
|
|
1062
|
+
const origin = request.headers.get("origin");
|
|
1063
|
+
if (origin === null) return void 0;
|
|
1064
|
+
const allowlist = allowed ?? [];
|
|
1065
|
+
return allowlist.includes(origin) ? void 0 : json({ error: "Origin is not allowed." }, 403);
|
|
1066
|
+
}
|
|
1067
|
+
function validateCallbackOrigin(request, openkeyHost) {
|
|
1068
|
+
const origin = request.headers.get("origin");
|
|
1069
|
+
return origin === null || origin === new URL(openkeyHost).origin ? void 0 : json({ error: "Origin is not allowed." }, 403);
|
|
1070
|
+
}
|
|
1071
|
+
function callbackCors(response, openkeyHost) {
|
|
1072
|
+
const headers = new Headers(response.headers);
|
|
1073
|
+
headers.set("access-control-allow-origin", new URL(openkeyHost).origin);
|
|
1074
|
+
headers.set("access-control-allow-methods", "POST, OPTIONS");
|
|
1075
|
+
headers.set("access-control-allow-headers", "content-type");
|
|
1076
|
+
headers.set("vary", "Origin");
|
|
1077
|
+
return new Response(response.body, { status: response.status, headers });
|
|
1078
|
+
}
|
|
1079
|
+
function json(value, status = 200) {
|
|
1080
|
+
return Response.json(value, { status, headers: { "cache-control": "no-store" } });
|
|
1081
|
+
}
|
|
1082
|
+
function methodNotAllowed(allow) {
|
|
1083
|
+
return new Response("Method not allowed", { status: 405, headers: { allow } });
|
|
1084
|
+
}
|
|
1085
|
+
function writeDiagnostic(message) {
|
|
1086
|
+
process.stderr.write(`[tinycloud-mcp-http] ${message}
|
|
1087
|
+
`);
|
|
1088
|
+
}
|
|
1089
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1090
|
+
0 && (module.exports = {
|
|
1091
|
+
createHostedMcpApp
|
|
1092
|
+
});
|
|
1093
|
+
//# sourceMappingURL=http.cjs.map
|