@tinycloud/mcp 0.8.3-beta.7 → 0.8.3-beta.8
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 +25 -0
- package/dist/cli.cjs +1 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/http-cli.cjs +182 -13
- package/dist/http-cli.cjs.map +1 -1
- package/dist/http-cli.js +176 -7
- package/dist/http-cli.js.map +1 -1
- package/dist/http.cjs +170 -12
- package/dist/http.cjs.map +1 -1
- package/dist/http.d.cts +22 -0
- package/dist/http.d.ts +22 -0
- package/dist/http.js +164 -6
- package/dist/http.js.map +1 -1
- package/dist/index.cjs +170 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +164 -6
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +1 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -522,7 +522,7 @@ function outcomeCode(result) {
|
|
|
522
522
|
// package.json
|
|
523
523
|
var package_default = {
|
|
524
524
|
name: "@tinycloud/mcp",
|
|
525
|
-
version: "0.8.3-beta.
|
|
525
|
+
version: "0.8.3-beta.8",
|
|
526
526
|
description: "TinyCloud delegated operations MCP server",
|
|
527
527
|
keywords: [
|
|
528
528
|
"tinycloud",
|
|
@@ -752,7 +752,9 @@ function createOpenKeyTokenVerifier(config) {
|
|
|
752
752
|
issuer: config.issuer,
|
|
753
753
|
audience: config.audience
|
|
754
754
|
});
|
|
755
|
-
|
|
755
|
+
const info = authInfo(token, payload, config);
|
|
756
|
+
if (config.tokenStatus !== void 0) await requireUsableStatus(config.tokenStatus, token);
|
|
757
|
+
return info;
|
|
756
758
|
} catch (error) {
|
|
757
759
|
if (error instanceof import_jose.errors.JOSEError) {
|
|
758
760
|
throw new import_server4.OAuthError(import_server4.OAuthErrorCode.InvalidToken, "The OpenKey access token is invalid.");
|
|
@@ -762,6 +764,16 @@ function createOpenKeyTokenVerifier(config) {
|
|
|
762
764
|
}
|
|
763
765
|
};
|
|
764
766
|
}
|
|
767
|
+
async function requireUsableStatus(checker, token) {
|
|
768
|
+
const status = await checker.tokenStatus(token);
|
|
769
|
+
if (status === "active") return;
|
|
770
|
+
if (status === "inactive") {
|
|
771
|
+
throw new import_server4.OAuthError(import_server4.OAuthErrorCode.InvalidToken, "The OpenKey access token is no longer active.");
|
|
772
|
+
}
|
|
773
|
+
if (!checker.toleratesUnknown(token)) {
|
|
774
|
+
throw new import_server4.OAuthError(import_server4.OAuthErrorCode.ServerError, "The OpenKey access token status is unavailable.");
|
|
775
|
+
}
|
|
776
|
+
}
|
|
765
777
|
function authInfo(token, payload, config) {
|
|
766
778
|
if (typeof payload.sub !== "string" || payload.sub.length === 0) {
|
|
767
779
|
throw new import_server4.OAuthError(import_server4.OAuthErrorCode.InvalidToken, "The OpenKey access token does not identify a subject.");
|
|
@@ -815,8 +827,152 @@ function firstString(...values) {
|
|
|
815
827
|
return values.find((value) => typeof value === "string" && value.length > 0);
|
|
816
828
|
}
|
|
817
829
|
|
|
818
|
-
// src/
|
|
830
|
+
// src/token-status.ts
|
|
819
831
|
var import_node_crypto = require("crypto");
|
|
832
|
+
var import_jose2 = require("jose");
|
|
833
|
+
var ACTIVE_CACHE_MS = 3e4;
|
|
834
|
+
var DEFAULT_TOKEN_LIFETIME_SECONDS = 300;
|
|
835
|
+
var DEFAULT_TIMEOUT_MS = 3e3;
|
|
836
|
+
var MAX_CACHED_TOKENS = 1e4;
|
|
837
|
+
function createTokenStatusChecker(config) {
|
|
838
|
+
const doFetch = config.fetch ?? fetch;
|
|
839
|
+
const clock = config.now ?? Date.now;
|
|
840
|
+
const log = config.log ?? ((message) => void process.stderr.write(`[tinycloud-mcp-http] ${message}
|
|
841
|
+
`));
|
|
842
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
843
|
+
const inactive = /* @__PURE__ */ new Map();
|
|
844
|
+
const active = /* @__PURE__ */ new Map();
|
|
845
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
846
|
+
const confirmedActive = /* @__PURE__ */ new Map();
|
|
847
|
+
let credentialsRefused = false;
|
|
848
|
+
async function introspect(token) {
|
|
849
|
+
const headers = {
|
|
850
|
+
accept: "application/json",
|
|
851
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
852
|
+
};
|
|
853
|
+
const body = new URLSearchParams({ token, token_type_hint: "access_token" });
|
|
854
|
+
if (config.authMethod === "basic") {
|
|
855
|
+
const encode = (value) => encodeURIComponent(value).replace(/%20/g, "+");
|
|
856
|
+
const credentials = `${encode(config.clientId)}:${encode(config.clientSecret)}`;
|
|
857
|
+
headers.authorization = `Basic ${Buffer.from(credentials).toString("base64")}`;
|
|
858
|
+
} else {
|
|
859
|
+
body.set("client_id", config.clientId);
|
|
860
|
+
body.set("client_secret", config.clientSecret);
|
|
861
|
+
}
|
|
862
|
+
try {
|
|
863
|
+
const response = await doFetch(config.endpoint, {
|
|
864
|
+
method: "POST",
|
|
865
|
+
headers,
|
|
866
|
+
body,
|
|
867
|
+
redirect: "error",
|
|
868
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
869
|
+
});
|
|
870
|
+
if (response.status === 401) return "credentials";
|
|
871
|
+
if (response.status !== 200) return "unknown";
|
|
872
|
+
const result = await response.json();
|
|
873
|
+
if (typeof result !== "object" || result === null || !("active" in result)) return "unknown";
|
|
874
|
+
if (result.active === false) return "inactive";
|
|
875
|
+
if (result.active !== true) return "unknown";
|
|
876
|
+
return answerMatchesToken(Object.fromEntries(Object.entries(result)), token, config.resourceUrl) ? "active" : "mismatch";
|
|
877
|
+
} catch {
|
|
878
|
+
return "unknown";
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
async function query(key, token) {
|
|
882
|
+
const outcome = await introspect(token);
|
|
883
|
+
if (outcome === "credentials") {
|
|
884
|
+
if (!credentialsRefused) {
|
|
885
|
+
log("OpenKey token introspection refused the MCP client credentials (401); token checks fail closed until it accepts them.");
|
|
886
|
+
}
|
|
887
|
+
credentialsRefused = true;
|
|
888
|
+
return "unknown";
|
|
889
|
+
}
|
|
890
|
+
if (outcome === "unknown") return outcome;
|
|
891
|
+
const now = clock();
|
|
892
|
+
prune(now);
|
|
893
|
+
if (outcome === "mismatch") {
|
|
894
|
+
inactive.set(key, tokenExpiryMs(token, now));
|
|
895
|
+
confirmedActive.delete(key);
|
|
896
|
+
return "inactive";
|
|
897
|
+
}
|
|
898
|
+
credentialsRefused = false;
|
|
899
|
+
if (outcome === "inactive") {
|
|
900
|
+
inactive.set(key, tokenExpiryMs(token, now));
|
|
901
|
+
confirmedActive.delete(key);
|
|
902
|
+
} else {
|
|
903
|
+
active.set(key, now + ACTIVE_CACHE_MS);
|
|
904
|
+
confirmedActive.delete(key);
|
|
905
|
+
confirmedActive.set(key, { at: now, expiresAt: tokenExpiryMs(token, now) });
|
|
906
|
+
}
|
|
907
|
+
return outcome;
|
|
908
|
+
}
|
|
909
|
+
function prune(now) {
|
|
910
|
+
for (const [key, until] of active) if (until <= now) active.delete(key);
|
|
911
|
+
for (const [key, until] of inactive) if (until <= now) inactive.delete(key);
|
|
912
|
+
for (const [key, entry] of confirmedActive) if (entry.expiresAt <= now) confirmedActive.delete(key);
|
|
913
|
+
for (const cache of [active, inactive, confirmedActive]) {
|
|
914
|
+
while (cache.size >= MAX_CACHED_TOKENS) {
|
|
915
|
+
const oldest = cache.keys().next().value;
|
|
916
|
+
if (oldest === void 0) break;
|
|
917
|
+
cache.delete(oldest);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return {
|
|
922
|
+
async tokenStatus(token) {
|
|
923
|
+
const key = (0, import_node_crypto.createHash)("sha256").update(token).digest("hex");
|
|
924
|
+
const now = clock();
|
|
925
|
+
const inactiveUntil = inactive.get(key);
|
|
926
|
+
if (inactiveUntil !== void 0 && inactiveUntil > now) return "inactive";
|
|
927
|
+
const activeUntil = active.get(key);
|
|
928
|
+
if (activeUntil !== void 0 && activeUntil > now) return "active";
|
|
929
|
+
const pending = inFlight.get(key);
|
|
930
|
+
if (pending !== void 0) return pending;
|
|
931
|
+
const asking = query(key, token).finally(() => inFlight.delete(key));
|
|
932
|
+
inFlight.set(key, asking);
|
|
933
|
+
return asking;
|
|
934
|
+
},
|
|
935
|
+
toleratesUnknown(token) {
|
|
936
|
+
if (credentialsRefused) return false;
|
|
937
|
+
const confirmed = confirmedActive.get((0, import_node_crypto.createHash)("sha256").update(token).digest("hex"));
|
|
938
|
+
if (confirmed === void 0) return false;
|
|
939
|
+
const now = clock();
|
|
940
|
+
return confirmed.expiresAt > now && now - confirmed.at < tokenLifetimeSeconds(token) * 1e3;
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
function answerMatchesToken(answer, token, resourceUrl) {
|
|
945
|
+
const jwt = claims(token);
|
|
946
|
+
if (typeof jwt.sub === "string" && answer.sub !== jwt.sub) return false;
|
|
947
|
+
const jwtClient = [jwt.azp, jwt.client_id].find((value) => typeof value === "string" && value.length > 0);
|
|
948
|
+
if (jwtClient !== void 0 && answer.client_id !== jwtClient) return false;
|
|
949
|
+
if (jwt.aud === void 0) return true;
|
|
950
|
+
const answered = audienceList(answer.aud);
|
|
951
|
+
const signed = audienceList(jwt.aud);
|
|
952
|
+
return answered.length > 0 && answered.includes(resourceUrl) && answered.every((entry) => signed.includes(entry));
|
|
953
|
+
}
|
|
954
|
+
function audienceList(value) {
|
|
955
|
+
const list = typeof value === "string" ? [value] : value;
|
|
956
|
+
return Array.isArray(list) && list.every((entry) => typeof entry === "string") ? list : [];
|
|
957
|
+
}
|
|
958
|
+
function tokenExpiryMs(token, now) {
|
|
959
|
+
const exp = claims(token).exp;
|
|
960
|
+
return typeof exp === "number" ? exp * 1e3 : now + DEFAULT_TOKEN_LIFETIME_SECONDS * 1e3;
|
|
961
|
+
}
|
|
962
|
+
function tokenLifetimeSeconds(token) {
|
|
963
|
+
const { exp, iat } = claims(token);
|
|
964
|
+
return typeof exp === "number" && typeof iat === "number" && exp > iat ? exp - iat : DEFAULT_TOKEN_LIFETIME_SECONDS;
|
|
965
|
+
}
|
|
966
|
+
function claims(token) {
|
|
967
|
+
try {
|
|
968
|
+
return (0, import_jose2.decodeJwt)(token);
|
|
969
|
+
} catch {
|
|
970
|
+
return {};
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// src/remote-store.ts
|
|
975
|
+
var import_node_crypto2 = require("crypto");
|
|
820
976
|
var import_promises = require("fs/promises");
|
|
821
977
|
var import_node_path3 = require("path");
|
|
822
978
|
var import_node_sdk2 = require("@tinycloud/node-sdk");
|
|
@@ -1408,7 +1564,7 @@ var RemoteTenantStore = class {
|
|
|
1408
1564
|
{ tenantId, approvalEpoch: tenant.approvalEpoch, kind, requestId, permissions, ownerDids }
|
|
1409
1565
|
);
|
|
1410
1566
|
if (reusable !== void 0) return reusable;
|
|
1411
|
-
const nonce = (0,
|
|
1567
|
+
const nonce = (0, import_node_crypto2.randomBytes)(24).toString("base64url");
|
|
1412
1568
|
const created = Date.now();
|
|
1413
1569
|
const pending = {
|
|
1414
1570
|
version: 1,
|
|
@@ -1433,7 +1589,7 @@ var RemoteTenantStore = class {
|
|
|
1433
1589
|
const expected = this.#signState(tenantId, nonce);
|
|
1434
1590
|
const supplied = Buffer.from(signature, "base64url");
|
|
1435
1591
|
const expectedBytes = Buffer.from(expected, "base64url");
|
|
1436
|
-
if (supplied.length !== expectedBytes.length || !(0,
|
|
1592
|
+
if (supplied.length !== expectedBytes.length || !(0, import_node_crypto2.timingSafeEqual)(supplied, expectedBytes)) {
|
|
1437
1593
|
throw new ApprovalRequestError("state_invalid", "Approval state is invalid.");
|
|
1438
1594
|
}
|
|
1439
1595
|
const stateRoot = (0, import_node_path3.join)(this.#config.stateDir, "tenants", tenantId);
|
|
@@ -1485,10 +1641,10 @@ var RemoteTenantStore = class {
|
|
|
1485
1641
|
return url.toString();
|
|
1486
1642
|
}
|
|
1487
1643
|
#tenantId(subject) {
|
|
1488
|
-
return (0,
|
|
1644
|
+
return (0, import_node_crypto2.createHmac)("sha256", this.#config.stateSecret).update(`tenant:${subject}`).digest("hex");
|
|
1489
1645
|
}
|
|
1490
1646
|
#signState(tenantId, nonce) {
|
|
1491
|
-
return (0,
|
|
1647
|
+
return (0, import_node_crypto2.createHmac)("sha256", this.#config.stateSecret).update(`approval:${tenantId}:${nonce}`).digest("base64url");
|
|
1492
1648
|
}
|
|
1493
1649
|
#profilePath(stateRoot) {
|
|
1494
1650
|
return (0, import_node_path3.join)(stateRoot, ".tinycloud", "profiles", PROFILE, "profile.json");
|
|
@@ -1590,7 +1746,7 @@ async function secureDirectory(path) {
|
|
|
1590
1746
|
async function secureWrite(path, value) {
|
|
1591
1747
|
const directory = (0, import_node_path3.dirname)(path);
|
|
1592
1748
|
await secureDirectory(directory);
|
|
1593
|
-
const temporary = `${path}.${process.pid}.${(0,
|
|
1749
|
+
const temporary = `${path}.${process.pid}.${(0, import_node_crypto2.randomBytes)(8).toString("hex")}.tmp`;
|
|
1594
1750
|
await (0, import_promises.writeFile)(temporary, `${JSON.stringify(value, jsonReplacer, 2)}
|
|
1595
1751
|
`, { mode: 384 });
|
|
1596
1752
|
await (0, import_promises.rename)(temporary, path);
|
|
@@ -1642,12 +1798,13 @@ var REQUIRED_SCOPE = "tinycloud:mcp";
|
|
|
1642
1798
|
function createHostedMcpApp(config) {
|
|
1643
1799
|
const resourceUrl = canonicalResourceUrl(config.publicUrl);
|
|
1644
1800
|
const pluginUrl = new URL("./chatgpt/mcp", resourceUrl);
|
|
1645
|
-
const
|
|
1801
|
+
const statusFor = (resource) => config.introspection === void 0 ? void 0 : createTokenStatusChecker({ ...config.introspection, resourceUrl: resource.toString() });
|
|
1802
|
+
const hosted = createHostedSurface(config, resourceUrl, "hosted", statusFor(resourceUrl));
|
|
1646
1803
|
const plugin = createHostedSurface({
|
|
1647
1804
|
...config,
|
|
1648
1805
|
stateDir: (0, import_node_path4.join)(config.stateDir, "chatgpt"),
|
|
1649
1806
|
approvalPath: new URL("./connect", pluginUrl).pathname
|
|
1650
|
-
}, pluginUrl, "public");
|
|
1807
|
+
}, pluginUrl, "public", statusFor(pluginUrl));
|
|
1651
1808
|
const pluginPaths = {
|
|
1652
1809
|
[pluginUrl.pathname]: true,
|
|
1653
1810
|
[new URL("./connect", pluginUrl).pathname]: true,
|
|
@@ -1661,7 +1818,7 @@ function createHostedMcpApp(config) {
|
|
|
1661
1818
|
}
|
|
1662
1819
|
};
|
|
1663
1820
|
}
|
|
1664
|
-
function createHostedSurface(config, resourceUrl, surface) {
|
|
1821
|
+
function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
|
|
1665
1822
|
const connectPath = config.approvalPath ?? "/connect";
|
|
1666
1823
|
const callbackPath = `${connectPath}/callback`;
|
|
1667
1824
|
const issuer = String(config.oauthMetadata.issuer);
|
|
@@ -1670,7 +1827,8 @@ function createHostedSurface(config, resourceUrl, surface) {
|
|
|
1670
1827
|
issuer,
|
|
1671
1828
|
audience: resourceUrl.toString(),
|
|
1672
1829
|
jwksUri,
|
|
1673
|
-
requiredScope: REQUIRED_SCOPE
|
|
1830
|
+
requiredScope: REQUIRED_SCOPE,
|
|
1831
|
+
...tokenStatus === void 0 ? {} : { tokenStatus }
|
|
1674
1832
|
});
|
|
1675
1833
|
const resourceMetadataUrl = (0, import_server5.getOAuthProtectedResourceMetadataUrl)(resourceUrl);
|
|
1676
1834
|
const authenticate = (0, import_server5.requireBearerAuth)({
|