@chessceo/mcp 0.5.0 → 0.6.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/dist/index.js +56 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -28,6 +28,26 @@ const UA = `chessceo-mcp/${process.env.npm_package_version ?? "0.1.0"} (+https:/
|
|
|
28
28
|
// per-request via AsyncLocalStorage so tool handlers can see it even
|
|
29
29
|
// though the MCP SDK's request handler doesn't know about HTTP.
|
|
30
30
|
const authContext = new AsyncLocalStorage();
|
|
31
|
+
// Tools that require an MCP token — cloud engine tools operate on the
|
|
32
|
+
// user's rented instances so we can't service them anonymously. The
|
|
33
|
+
// streamable-http transport uses this list to decide whether to trigger
|
|
34
|
+
// the OAuth discovery flow via 401 + WWW-Authenticate before the SDK
|
|
35
|
+
// gets a chance to handle the call.
|
|
36
|
+
const AUTHED_TOOLS = new Set([
|
|
37
|
+
"start_cloud_engine",
|
|
38
|
+
"list_cloud_engines",
|
|
39
|
+
"stop_cloud_engine",
|
|
40
|
+
"cloud_analyse",
|
|
41
|
+
]);
|
|
42
|
+
function isAuthedToolCall(body) {
|
|
43
|
+
if (!body || typeof body !== "object")
|
|
44
|
+
return false;
|
|
45
|
+
const b = body;
|
|
46
|
+
if (b.method !== "tools/call")
|
|
47
|
+
return false;
|
|
48
|
+
const name = b.params?.name;
|
|
49
|
+
return typeof name === "string" && AUTHED_TOOLS.has(name);
|
|
50
|
+
}
|
|
31
51
|
function resolveAuthHeader() {
|
|
32
52
|
const store = authContext.getStore();
|
|
33
53
|
if (store?.authHeader)
|
|
@@ -649,14 +669,48 @@ else if (transportKind === "http" || transportKind === "streamable-http") {
|
|
|
649
669
|
res.end("ok\n");
|
|
650
670
|
return;
|
|
651
671
|
}
|
|
652
|
-
// Everything else must hit the MCP path.
|
|
653
672
|
const urlPath = (req.url ?? "").split("?")[0];
|
|
673
|
+
// RFC 9728 protected-resource metadata. MCP hosts (claude.ai, ChatGPT)
|
|
674
|
+
// fetch this after receiving a 401 with WWW-Authenticate below; it
|
|
675
|
+
// points them at chess.ceo's OAuth 2.1 authorization server, which
|
|
676
|
+
// handles registration (DCR), consent, and token issuance.
|
|
677
|
+
if (req.method === "GET" && urlPath === "/.well-known/oauth-protected-resource") {
|
|
678
|
+
res.statusCode = 200;
|
|
679
|
+
res.setHeader("Content-Type", "application/json");
|
|
680
|
+
res.setHeader("Cache-Control", "public, max-age=3600");
|
|
681
|
+
res.end(JSON.stringify({
|
|
682
|
+
resource: "https://mcp.chess.ceo/mcp",
|
|
683
|
+
authorization_servers: ["https://chess.ceo"],
|
|
684
|
+
scopes_supported: ["agent"],
|
|
685
|
+
bearer_methods_supported: ["header"],
|
|
686
|
+
}));
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
// Everything else must hit the MCP path.
|
|
654
690
|
if (urlPath !== path) {
|
|
655
691
|
res.statusCode = 404;
|
|
656
692
|
res.end();
|
|
657
693
|
return;
|
|
658
694
|
}
|
|
659
695
|
try {
|
|
696
|
+
const body = req.method === "POST" ? await readBody(req) : undefined;
|
|
697
|
+
const authHeader = req.headers["authorization"];
|
|
698
|
+
const authHeaderStr = Array.isArray(authHeader) ? authHeader[0] : authHeader;
|
|
699
|
+
// If the caller is invoking an authed tool without a token, respond
|
|
700
|
+
// with 401 + WWW-Authenticate pointing at RFC 9728 metadata BEFORE
|
|
701
|
+
// handing off to the MCP SDK — MCP hosts (claude.ai, ChatGPT) look
|
|
702
|
+
// for this header at the HTTP layer to auto-start OAuth discovery.
|
|
703
|
+
if (!authHeaderStr && isAuthedToolCall(body)) {
|
|
704
|
+
res.statusCode = 401;
|
|
705
|
+
res.setHeader("WWW-Authenticate", `Bearer realm="chess.ceo", resource_metadata="https://mcp.chess.ceo/.well-known/oauth-protected-resource"`);
|
|
706
|
+
res.setHeader("Content-Type", "application/json");
|
|
707
|
+
res.end(JSON.stringify({
|
|
708
|
+
jsonrpc: "2.0",
|
|
709
|
+
error: { code: -32001, message: "authentication required" },
|
|
710
|
+
id: body?.id ?? null,
|
|
711
|
+
}));
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
660
714
|
// Stateless: one transport per request, no session store. Simpler,
|
|
661
715
|
// scales trivially, matches how claude.ai / ChatGPT connectors call.
|
|
662
716
|
const transport = new StreamableHTTPServerTransport({
|
|
@@ -665,12 +719,10 @@ else if (transportKind === "http" || transportKind === "streamable-http") {
|
|
|
665
719
|
});
|
|
666
720
|
res.on("close", () => transport.close());
|
|
667
721
|
await server.connect(transport);
|
|
668
|
-
const body = req.method === "POST" ? await readBody(req) : undefined;
|
|
669
722
|
// Forward the caller's Authorization header down to tool handlers so
|
|
670
723
|
// they can attach it when calling authenticated backend endpoints.
|
|
671
724
|
// AsyncLocalStorage survives every await inside the tool handler.
|
|
672
|
-
|
|
673
|
-
await authContext.run({ authHeader: Array.isArray(authHeader) ? authHeader[0] : authHeader }, async () => {
|
|
725
|
+
await authContext.run({ authHeader: authHeaderStr }, async () => {
|
|
674
726
|
await transport.handleRequest(req, res, body);
|
|
675
727
|
});
|
|
676
728
|
}
|
package/package.json
CHANGED