@tinycloud/mcp 0.8.3-beta.6 → 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 +3 -3
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +3 -3
- package/dist/cli.js.map +1 -1
- package/dist/http-cli.cjs +184 -15
- package/dist/http-cli.cjs.map +1 -1
- package/dist/http-cli.js +178 -9
- package/dist/http-cli.js.map +1 -1
- package/dist/http.cjs +172 -14
- 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 +166 -8
- package/dist/http.js.map +1 -1
- package/dist/index.cjs +172 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +166 -8
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +3 -3
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +3 -3
- package/dist/server.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -485,7 +485,7 @@ function outcomeCode(result) {
|
|
|
485
485
|
// package.json
|
|
486
486
|
var package_default = {
|
|
487
487
|
name: "@tinycloud/mcp",
|
|
488
|
-
version: "0.8.3-beta.
|
|
488
|
+
version: "0.8.3-beta.8",
|
|
489
489
|
description: "TinyCloud delegated operations MCP server",
|
|
490
490
|
keywords: [
|
|
491
491
|
"tinycloud",
|
|
@@ -556,9 +556,9 @@ var package_default = {
|
|
|
556
556
|
dependencies: {
|
|
557
557
|
"@modelcontextprotocol/node": "2.0.0-beta.5",
|
|
558
558
|
"@modelcontextprotocol/server": "2.0.0-beta.5",
|
|
559
|
-
"@tinycloud/node-sdk": "3.8.0-beta.
|
|
559
|
+
"@tinycloud/node-sdk": "3.8.0-beta.6",
|
|
560
560
|
"@tinycloud/node-sdk-wasm": "1.7.8",
|
|
561
|
-
"@tinycloud/operations": "0.7.7-beta.
|
|
561
|
+
"@tinycloud/operations": "0.7.7-beta.7",
|
|
562
562
|
jose: "6.2.4"
|
|
563
563
|
},
|
|
564
564
|
devDependencies: {
|
|
@@ -726,7 +726,9 @@ function createOpenKeyTokenVerifier(config) {
|
|
|
726
726
|
issuer: config.issuer,
|
|
727
727
|
audience: config.audience
|
|
728
728
|
});
|
|
729
|
-
|
|
729
|
+
const info = authInfo(token, payload, config);
|
|
730
|
+
if (config.tokenStatus !== void 0) await requireUsableStatus(config.tokenStatus, token);
|
|
731
|
+
return info;
|
|
730
732
|
} catch (error) {
|
|
731
733
|
if (error instanceof errors.JOSEError) {
|
|
732
734
|
throw new OAuthError(OAuthErrorCode.InvalidToken, "The OpenKey access token is invalid.");
|
|
@@ -736,6 +738,16 @@ function createOpenKeyTokenVerifier(config) {
|
|
|
736
738
|
}
|
|
737
739
|
};
|
|
738
740
|
}
|
|
741
|
+
async function requireUsableStatus(checker, token) {
|
|
742
|
+
const status = await checker.tokenStatus(token);
|
|
743
|
+
if (status === "active") return;
|
|
744
|
+
if (status === "inactive") {
|
|
745
|
+
throw new OAuthError(OAuthErrorCode.InvalidToken, "The OpenKey access token is no longer active.");
|
|
746
|
+
}
|
|
747
|
+
if (!checker.toleratesUnknown(token)) {
|
|
748
|
+
throw new OAuthError(OAuthErrorCode.ServerError, "The OpenKey access token status is unavailable.");
|
|
749
|
+
}
|
|
750
|
+
}
|
|
739
751
|
function authInfo(token, payload, config) {
|
|
740
752
|
if (typeof payload.sub !== "string" || payload.sub.length === 0) {
|
|
741
753
|
throw new OAuthError(OAuthErrorCode.InvalidToken, "The OpenKey access token does not identify a subject.");
|
|
@@ -789,6 +801,150 @@ function firstString(...values) {
|
|
|
789
801
|
return values.find((value) => typeof value === "string" && value.length > 0);
|
|
790
802
|
}
|
|
791
803
|
|
|
804
|
+
// src/token-status.ts
|
|
805
|
+
import { createHash } from "crypto";
|
|
806
|
+
import { decodeJwt } from "jose";
|
|
807
|
+
var ACTIVE_CACHE_MS = 3e4;
|
|
808
|
+
var DEFAULT_TOKEN_LIFETIME_SECONDS = 300;
|
|
809
|
+
var DEFAULT_TIMEOUT_MS = 3e3;
|
|
810
|
+
var MAX_CACHED_TOKENS = 1e4;
|
|
811
|
+
function createTokenStatusChecker(config) {
|
|
812
|
+
const doFetch = config.fetch ?? fetch;
|
|
813
|
+
const clock = config.now ?? Date.now;
|
|
814
|
+
const log = config.log ?? ((message) => void process.stderr.write(`[tinycloud-mcp-http] ${message}
|
|
815
|
+
`));
|
|
816
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
817
|
+
const inactive = /* @__PURE__ */ new Map();
|
|
818
|
+
const active = /* @__PURE__ */ new Map();
|
|
819
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
820
|
+
const confirmedActive = /* @__PURE__ */ new Map();
|
|
821
|
+
let credentialsRefused = false;
|
|
822
|
+
async function introspect(token) {
|
|
823
|
+
const headers = {
|
|
824
|
+
accept: "application/json",
|
|
825
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
826
|
+
};
|
|
827
|
+
const body = new URLSearchParams({ token, token_type_hint: "access_token" });
|
|
828
|
+
if (config.authMethod === "basic") {
|
|
829
|
+
const encode = (value) => encodeURIComponent(value).replace(/%20/g, "+");
|
|
830
|
+
const credentials = `${encode(config.clientId)}:${encode(config.clientSecret)}`;
|
|
831
|
+
headers.authorization = `Basic ${Buffer.from(credentials).toString("base64")}`;
|
|
832
|
+
} else {
|
|
833
|
+
body.set("client_id", config.clientId);
|
|
834
|
+
body.set("client_secret", config.clientSecret);
|
|
835
|
+
}
|
|
836
|
+
try {
|
|
837
|
+
const response = await doFetch(config.endpoint, {
|
|
838
|
+
method: "POST",
|
|
839
|
+
headers,
|
|
840
|
+
body,
|
|
841
|
+
redirect: "error",
|
|
842
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
843
|
+
});
|
|
844
|
+
if (response.status === 401) return "credentials";
|
|
845
|
+
if (response.status !== 200) return "unknown";
|
|
846
|
+
const result = await response.json();
|
|
847
|
+
if (typeof result !== "object" || result === null || !("active" in result)) return "unknown";
|
|
848
|
+
if (result.active === false) return "inactive";
|
|
849
|
+
if (result.active !== true) return "unknown";
|
|
850
|
+
return answerMatchesToken(Object.fromEntries(Object.entries(result)), token, config.resourceUrl) ? "active" : "mismatch";
|
|
851
|
+
} catch {
|
|
852
|
+
return "unknown";
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
async function query(key, token) {
|
|
856
|
+
const outcome = await introspect(token);
|
|
857
|
+
if (outcome === "credentials") {
|
|
858
|
+
if (!credentialsRefused) {
|
|
859
|
+
log("OpenKey token introspection refused the MCP client credentials (401); token checks fail closed until it accepts them.");
|
|
860
|
+
}
|
|
861
|
+
credentialsRefused = true;
|
|
862
|
+
return "unknown";
|
|
863
|
+
}
|
|
864
|
+
if (outcome === "unknown") return outcome;
|
|
865
|
+
const now = clock();
|
|
866
|
+
prune(now);
|
|
867
|
+
if (outcome === "mismatch") {
|
|
868
|
+
inactive.set(key, tokenExpiryMs(token, now));
|
|
869
|
+
confirmedActive.delete(key);
|
|
870
|
+
return "inactive";
|
|
871
|
+
}
|
|
872
|
+
credentialsRefused = false;
|
|
873
|
+
if (outcome === "inactive") {
|
|
874
|
+
inactive.set(key, tokenExpiryMs(token, now));
|
|
875
|
+
confirmedActive.delete(key);
|
|
876
|
+
} else {
|
|
877
|
+
active.set(key, now + ACTIVE_CACHE_MS);
|
|
878
|
+
confirmedActive.delete(key);
|
|
879
|
+
confirmedActive.set(key, { at: now, expiresAt: tokenExpiryMs(token, now) });
|
|
880
|
+
}
|
|
881
|
+
return outcome;
|
|
882
|
+
}
|
|
883
|
+
function prune(now) {
|
|
884
|
+
for (const [key, until] of active) if (until <= now) active.delete(key);
|
|
885
|
+
for (const [key, until] of inactive) if (until <= now) inactive.delete(key);
|
|
886
|
+
for (const [key, entry] of confirmedActive) if (entry.expiresAt <= now) confirmedActive.delete(key);
|
|
887
|
+
for (const cache of [active, inactive, confirmedActive]) {
|
|
888
|
+
while (cache.size >= MAX_CACHED_TOKENS) {
|
|
889
|
+
const oldest = cache.keys().next().value;
|
|
890
|
+
if (oldest === void 0) break;
|
|
891
|
+
cache.delete(oldest);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return {
|
|
896
|
+
async tokenStatus(token) {
|
|
897
|
+
const key = createHash("sha256").update(token).digest("hex");
|
|
898
|
+
const now = clock();
|
|
899
|
+
const inactiveUntil = inactive.get(key);
|
|
900
|
+
if (inactiveUntil !== void 0 && inactiveUntil > now) return "inactive";
|
|
901
|
+
const activeUntil = active.get(key);
|
|
902
|
+
if (activeUntil !== void 0 && activeUntil > now) return "active";
|
|
903
|
+
const pending = inFlight.get(key);
|
|
904
|
+
if (pending !== void 0) return pending;
|
|
905
|
+
const asking = query(key, token).finally(() => inFlight.delete(key));
|
|
906
|
+
inFlight.set(key, asking);
|
|
907
|
+
return asking;
|
|
908
|
+
},
|
|
909
|
+
toleratesUnknown(token) {
|
|
910
|
+
if (credentialsRefused) return false;
|
|
911
|
+
const confirmed = confirmedActive.get(createHash("sha256").update(token).digest("hex"));
|
|
912
|
+
if (confirmed === void 0) return false;
|
|
913
|
+
const now = clock();
|
|
914
|
+
return confirmed.expiresAt > now && now - confirmed.at < tokenLifetimeSeconds(token) * 1e3;
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
function answerMatchesToken(answer, token, resourceUrl) {
|
|
919
|
+
const jwt = claims(token);
|
|
920
|
+
if (typeof jwt.sub === "string" && answer.sub !== jwt.sub) return false;
|
|
921
|
+
const jwtClient = [jwt.azp, jwt.client_id].find((value) => typeof value === "string" && value.length > 0);
|
|
922
|
+
if (jwtClient !== void 0 && answer.client_id !== jwtClient) return false;
|
|
923
|
+
if (jwt.aud === void 0) return true;
|
|
924
|
+
const answered = audienceList(answer.aud);
|
|
925
|
+
const signed = audienceList(jwt.aud);
|
|
926
|
+
return answered.length > 0 && answered.includes(resourceUrl) && answered.every((entry) => signed.includes(entry));
|
|
927
|
+
}
|
|
928
|
+
function audienceList(value) {
|
|
929
|
+
const list = typeof value === "string" ? [value] : value;
|
|
930
|
+
return Array.isArray(list) && list.every((entry) => typeof entry === "string") ? list : [];
|
|
931
|
+
}
|
|
932
|
+
function tokenExpiryMs(token, now) {
|
|
933
|
+
const exp = claims(token).exp;
|
|
934
|
+
return typeof exp === "number" ? exp * 1e3 : now + DEFAULT_TOKEN_LIFETIME_SECONDS * 1e3;
|
|
935
|
+
}
|
|
936
|
+
function tokenLifetimeSeconds(token) {
|
|
937
|
+
const { exp, iat } = claims(token);
|
|
938
|
+
return typeof exp === "number" && typeof iat === "number" && exp > iat ? exp - iat : DEFAULT_TOKEN_LIFETIME_SECONDS;
|
|
939
|
+
}
|
|
940
|
+
function claims(token) {
|
|
941
|
+
try {
|
|
942
|
+
return decodeJwt(token);
|
|
943
|
+
} catch {
|
|
944
|
+
return {};
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
792
948
|
// src/remote-store.ts
|
|
793
949
|
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
794
950
|
import { chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
|
|
@@ -1625,12 +1781,13 @@ var REQUIRED_SCOPE = "tinycloud:mcp";
|
|
|
1625
1781
|
function createHostedMcpApp(config) {
|
|
1626
1782
|
const resourceUrl = canonicalResourceUrl(config.publicUrl);
|
|
1627
1783
|
const pluginUrl = new URL("./chatgpt/mcp", resourceUrl);
|
|
1628
|
-
const
|
|
1784
|
+
const statusFor = (resource) => config.introspection === void 0 ? void 0 : createTokenStatusChecker({ ...config.introspection, resourceUrl: resource.toString() });
|
|
1785
|
+
const hosted = createHostedSurface(config, resourceUrl, "hosted", statusFor(resourceUrl));
|
|
1629
1786
|
const plugin = createHostedSurface({
|
|
1630
1787
|
...config,
|
|
1631
1788
|
stateDir: join3(config.stateDir, "chatgpt"),
|
|
1632
1789
|
approvalPath: new URL("./connect", pluginUrl).pathname
|
|
1633
|
-
}, pluginUrl, "public");
|
|
1790
|
+
}, pluginUrl, "public", statusFor(pluginUrl));
|
|
1634
1791
|
const pluginPaths = {
|
|
1635
1792
|
[pluginUrl.pathname]: true,
|
|
1636
1793
|
[new URL("./connect", pluginUrl).pathname]: true,
|
|
@@ -1644,7 +1801,7 @@ function createHostedMcpApp(config) {
|
|
|
1644
1801
|
}
|
|
1645
1802
|
};
|
|
1646
1803
|
}
|
|
1647
|
-
function createHostedSurface(config, resourceUrl, surface) {
|
|
1804
|
+
function createHostedSurface(config, resourceUrl, surface, tokenStatus) {
|
|
1648
1805
|
const connectPath = config.approvalPath ?? "/connect";
|
|
1649
1806
|
const callbackPath = `${connectPath}/callback`;
|
|
1650
1807
|
const issuer = String(config.oauthMetadata.issuer);
|
|
@@ -1653,7 +1810,8 @@ function createHostedSurface(config, resourceUrl, surface) {
|
|
|
1653
1810
|
issuer,
|
|
1654
1811
|
audience: resourceUrl.toString(),
|
|
1655
1812
|
jwksUri,
|
|
1656
|
-
requiredScope: REQUIRED_SCOPE
|
|
1813
|
+
requiredScope: REQUIRED_SCOPE,
|
|
1814
|
+
...tokenStatus === void 0 ? {} : { tokenStatus }
|
|
1657
1815
|
});
|
|
1658
1816
|
const resourceMetadataUrl = getOAuthProtectedResourceMetadataUrl(resourceUrl);
|
|
1659
1817
|
const authenticate = requireBearerAuth({
|