hermoso 0.1.139 → 0.1.140

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 (3) hide show
  1. package/mcp/http.mjs +29 -4
  2. package/mcp/tools.mjs +539 -394
  3. package/package.json +1 -1
package/mcp/http.mjs CHANGED
@@ -32,12 +32,37 @@ export function mountRemoteMcp(app, { verifyBearer, publicBaseUrl } = {}) {
32
32
  // RFC 9728 protected-resource metadata — tells Claude.ai where to get a token. (Authorization-server metadata
33
33
  // is served by the auth provider itself, e.g. Firebase/your IdP.) Scopes match the AS metadata + minted token
34
34
  // (mcp/oauth.mjs): hermoso.research / hermoso.generate.
35
- app.get('/.well-known/oauth-protected-resource', (req, res) => res.json({
36
- resource: `${BASE}/mcp`,
35
+ // ── SERVED AT BOTH URL FORMS, FROM ONE HANDLER (2026-08-20) ──────────────────────────────────────────────────
36
+ // The resource identifier is `${BASE}/mcp`, which HAS a path component — so RFC 9728 §3.1 derives its metadata
37
+ // URL by INSERTING the well-known suffix before that path ("any terminating slash (/) following the host
38
+ // component MUST be removed before inserting /.well-known/ and the well-known URI path suffix between the host
39
+ // component and the path"), i.e. /.well-known/oauth-protected-resource/mcp. We served only the ROOT form, and
40
+ // that was not a cosmetic gap — it was a DEAD END for any client that does not use our WWW-Authenticate hint,
41
+ // because RFC 9728 §3.3 applies a DIFFERENT validation rule depending on how the client arrived:
42
+ // • via the `resource_metadata` hint → `resource` MUST equal the URL used to reach the resource server
43
+ // (`${BASE}/mcp`). Our document satisfies this, which is why Claude connects today.
44
+ // • by CONSTRUCTING the well-known URL → `resource` MUST equal the identifier the suffix was inserted into.
45
+ // From the ROOT form that identifier is `${BASE}` (no /mcp) while our document says `${BASE}/mcp`, and the
46
+ // rule is "the data contained in the response MUST NOT be used". So a hint-less client had nowhere to land:
47
+ // the suffixed URL 404'd, and the root URL it fell back to failed validation.
48
+ // Serving BOTH is explicitly sanctioned — the MCP spec's fallback order is suffixed-then-root ("Serve metadata
49
+ // at a well-known URI … either: At the path of the server's MCP endpoint … or At the root"). ONE handler, never
50
+ // a second copy of the document: two copies drift, and a stale one is worse than a 404.
51
+ //
52
+ // DELIBERATELY ASYMMETRIC with /.well-known/oauth-authorization-server (mcp/oauth.mjs), which is NOT aliased:
53
+ // its `issuer` is BASE with NO path component, so RFC 8414 §3.1 puts its metadata at the root URL, and §3.3
54
+ // would force a client to REJECT the identical document served under a /mcp suffix (it would have constructed
55
+ // that URL from issuer identifier `${BASE}/mcp`, which is not what the document says). The 404 there is
56
+ // CORRECT and is what lets a probing client fall through cleanly. Alias it only if `issuer` ever grows a path.
57
+ const MCP_PATH = '/mcp'; // the resource's path component — app.all(MCP_PATH) below
58
+ const protectedResourceMetadata = (req, res) => res.json({
59
+ resource: `${BASE}${MCP_PATH}`,
37
60
  authorization_servers: [process.env.HEIST_OAUTH_ISSUER].filter(Boolean),
38
61
  scopes_supported: ['hermoso.research', 'hermoso.generate'],
39
62
  bearer_methods_supported: ['header'],
40
- }));
63
+ });
64
+ app.get('/.well-known/oauth-protected-resource', protectedResourceMetadata);
65
+ app.get(`/.well-known/oauth-protected-resource${MCP_PATH}`, protectedResourceMetadata);
41
66
 
42
67
  // Per-session Streamable-HTTP transports. Each authenticated session gets its own McpServer with the same tools.
43
68
  //
@@ -131,7 +156,7 @@ export function mountRemoteMcp(app, { verifyBearer, publicBaseUrl } = {}) {
131
156
  await transport.handleRequest(req, res, req.body);
132
157
  }
133
158
 
134
- app.all('/mcp', async (req, res) => {
159
+ app.all(MCP_PATH, async (req, res) => {
135
160
  const auth = req.headers.authorization || '';
136
161
  const token = auth.startsWith('Bearer ') ? auth.slice(7) : '';
137
162
  const user = token ? await verifyBearer(token).catch(() => null) : null;