@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.
Files changed (2) hide show
  1. package/dist/index.js +56 -4
  2. 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
- const authHeader = req.headers["authorization"];
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chessceo/mcp",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Model Context Protocol server for chess.ceo — 11.7M+ games, ~1.5M FIDE player profiles, opening preparation, live broadcasts.",
5
5
  "type": "module",
6
6
  "bin": {