@exposify/express 0.3.15 → 0.3.16

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 CHANGED
@@ -192,6 +192,18 @@ decide what they can do. The startup log says which it is
192
192
  agents sign in" page. Adding a sign-in method there takes effect within a
193
193
  minute.
194
194
 
195
+ ### The `<ConnectAgents />` component
196
+
197
+ With the hosted broker on, `/mcp` also answers [`@exposify/react`](https://www.npmjs.com/package/@exposify/react)'s
198
+ `<ConnectAgents />` component — a "Connect your AI assistant" UI you can drop
199
+ into your own app. The component `fetch`es your `/mcp` with a custom `Accept`
200
+ header and gets back JSON (your app name, look, and ready-to-render install
201
+ clients). `aiReady()` serves it automatically, **before the auth gate**, so
202
+ the component works signed out, and with permissive CORS so it can live on a
203
+ different origin than your API. There's nothing to add — it's on whenever the
204
+ hosted broker is. Turn the component off from the dashboard switch (it then
205
+ renders nothing).
206
+
195
207
  ## "Click Connect and log in" (OAuth)
196
208
 
197
209
  By default the MCP client has to be handed a token by hand. If your app's
package/dist/index.cjs CHANGED
@@ -574,12 +574,13 @@ function aiReady(options = {}) {
574
574
  const gate = cfg.oauth ? (0, import_core6.createOAuthGate)(cfg.oauth) : null;
575
575
  const metadataPaths = cfg.oauth ? prmPaths(cfg.oauth.resource) : /* @__PURE__ */ new Set();
576
576
  const brokerGate = !gate && cfg.docsAuth === "required" ? (0, import_core6.createBrokerGate)(cfg.auth) : null;
577
- const resourceOf = (req, path, mcpPath) => {
577
+ const publicMcpUrl = (req, path, mcpPath) => {
578
578
  const original = String(req.originalUrl ?? req.url).split("?")[0];
579
579
  const prefix = original.endsWith(path) ? original.slice(0, original.length - path.length) : "";
580
580
  const local = `${req.protocol ?? "http"}://${req.headers.host ?? "localhost"}${prefix}${mcpPath}`;
581
- return brokerGate.resourceFor((0, import_core6.publicRequestUrl)(local, headersOf(req), { trustForwardedHost: trustsProxy(req) }));
581
+ return (0, import_core6.publicRequestUrl)(local, headersOf(req), { trustForwardedHost: trustsProxy(req) });
582
582
  };
583
+ const resourceOf = (req, path, mcpPath) => brokerGate.resourceFor(publicMcpUrl(req, path, mcpPath));
583
584
  const cwd = options.cwd ?? process.cwd();
584
585
  const mounts = options.mounts;
585
586
  const brokerInfo = (0, import_core6.hostedResolverInfo)(cfg.auth);
@@ -698,62 +699,73 @@ function aiReady(options = {}) {
698
699
  return;
699
700
  }
700
701
  if (path !== MCP_PATH && !path.startsWith(`${MCP_PATH}/`)) return next();
701
- if (disabled) {
702
- res.writeHead(503).end("agemno is disabled due to an internal error");
702
+ const handleMcp = () => {
703
+ if (disabled) {
704
+ res.writeHead(503).end("agemno is disabled due to an internal error");
705
+ return;
706
+ }
707
+ const authorized = gate ? gate.check(headersOf(req)).then(async (result) => {
708
+ if (result.ok) return { claims: result.claims };
709
+ await sendWebResponse(res, result.response);
710
+ return null;
711
+ }, (err) => {
712
+ console.warn("[agemno] oauth gate failed:", err);
713
+ if (!res.headersSent) res.writeHead(503).end("authorization check failed");
714
+ return null;
715
+ }) : brokerGate ? report(req, resourceOf(req, path, MCP_PATH)).then(async (verdict) => {
716
+ if (verdict === "open") return {};
717
+ const result = await brokerGate.check(headersOf(req), resourceOf(req, path, MCP_PATH));
718
+ if (result.ok) return {};
719
+ await sendWebResponse(res, result.response);
720
+ return null;
721
+ }).catch((err) => {
722
+ console.warn("[agemno] broker check failed:", err);
723
+ if (!res.headersSent) res.writeHead(503).end("authorization check failed");
724
+ return null;
725
+ }) : Promise.resolve({});
726
+ authorized.then((authContext) => {
727
+ if (!authContext) return;
728
+ handlerPromise ??= initialize(
729
+ req.app,
730
+ cfg,
731
+ cwd,
732
+ mounts,
733
+ { descriptions, openapi },
734
+ (surface) => logSurface(surface.routes, surface.docs.length, "updated: routes registered after startup")
735
+ );
736
+ return handlerPromise.then(
737
+ (handler) => (
738
+ // Tool calls made while handling this request reach the app with
739
+ // this caller's address and forwarding headers (see surface.ts).
740
+ runAsCaller(callerOf(req), () => handler(req, res, authContext))
741
+ )
742
+ );
743
+ }).catch((err) => {
744
+ disabled = true;
745
+ handlerPromise = null;
746
+ if (!warned) {
747
+ warned = true;
748
+ console.error(
749
+ (0, import_core6.formatFailed)(
750
+ LOG_PREFIX,
751
+ errorMessage(err),
752
+ `${MCP_PATH} now answers 503 until the process restarts (your own routes are unaffected); full error below`
753
+ ),
754
+ err
755
+ );
756
+ }
757
+ if (!res.headersSent) res.writeHead(503).end("agemno failed to initialize");
758
+ });
759
+ };
760
+ if (brokerInfo) {
761
+ const mcpUrl = brokerInfo.resource ?? (brokerGate ? resourceOf(req, path, MCP_PATH) : publicMcpUrl(req, path, MCP_PATH));
762
+ void (0, import_core6.serveConnectInfo)(new Request(mcpUrl, { method: req.method, headers: headersOf(req) }), brokerInfo, mcpUrl).then((info) => info ? sendWebResponse(res, info) : handleMcp()).catch((err) => {
763
+ console.warn("[agemno] connect-info failed:", err);
764
+ handleMcp();
765
+ });
703
766
  return;
704
767
  }
705
- const authorized = gate ? gate.check(headersOf(req)).then(async (result) => {
706
- if (result.ok) return { claims: result.claims };
707
- await sendWebResponse(res, result.response);
708
- return null;
709
- }, (err) => {
710
- console.warn("[agemno] oauth gate failed:", err);
711
- if (!res.headersSent) res.writeHead(503).end("authorization check failed");
712
- return null;
713
- }) : brokerGate ? report(req, resourceOf(req, path, MCP_PATH)).then(async (verdict) => {
714
- if (verdict === "open") return {};
715
- const result = await brokerGate.check(headersOf(req), resourceOf(req, path, MCP_PATH));
716
- if (result.ok) return {};
717
- await sendWebResponse(res, result.response);
718
- return null;
719
- }).catch((err) => {
720
- console.warn("[agemno] broker check failed:", err);
721
- if (!res.headersSent) res.writeHead(503).end("authorization check failed");
722
- return null;
723
- }) : Promise.resolve({});
724
- authorized.then((authContext) => {
725
- if (!authContext) return;
726
- handlerPromise ??= initialize(
727
- req.app,
728
- cfg,
729
- cwd,
730
- mounts,
731
- { descriptions, openapi },
732
- (surface) => logSurface(surface.routes, surface.docs.length, "updated: routes registered after startup")
733
- );
734
- return handlerPromise.then(
735
- (handler) => (
736
- // Tool calls made while handling this request reach the app with
737
- // this caller's address and forwarding headers (see surface.ts).
738
- runAsCaller(callerOf(req), () => handler(req, res, authContext))
739
- )
740
- );
741
- }).catch((err) => {
742
- disabled = true;
743
- handlerPromise = null;
744
- if (!warned) {
745
- warned = true;
746
- console.error(
747
- (0, import_core6.formatFailed)(
748
- LOG_PREFIX,
749
- errorMessage(err),
750
- `${MCP_PATH} now answers 503 until the process restarts (your own routes are unaffected); full error below`
751
- ),
752
- err
753
- );
754
- }
755
- if (!res.headersSent) res.writeHead(503).end("agemno failed to initialize");
756
- });
768
+ handleMcp();
757
769
  };
758
770
  async function serveLoginPage(req, res, login) {
759
771
  const original = String(req.originalUrl ?? req.url);
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  loginResultFromResponse,
25
25
  publicRequestUrl,
26
26
  reportAppLogin,
27
+ serveConnectInfo,
27
28
  verifyFields as verifyFields2,
28
29
  resolveOptions,
29
30
  routeToolsPhrase,
@@ -582,12 +583,13 @@ function aiReady(options = {}) {
582
583
  const gate = cfg.oauth ? createOAuthGate(cfg.oauth) : null;
583
584
  const metadataPaths = cfg.oauth ? prmPaths(cfg.oauth.resource) : /* @__PURE__ */ new Set();
584
585
  const brokerGate = !gate && cfg.docsAuth === "required" ? createBrokerGate(cfg.auth) : null;
585
- const resourceOf = (req, path, mcpPath) => {
586
+ const publicMcpUrl = (req, path, mcpPath) => {
586
587
  const original = String(req.originalUrl ?? req.url).split("?")[0];
587
588
  const prefix = original.endsWith(path) ? original.slice(0, original.length - path.length) : "";
588
589
  const local = `${req.protocol ?? "http"}://${req.headers.host ?? "localhost"}${prefix}${mcpPath}`;
589
- return brokerGate.resourceFor(publicRequestUrl(local, headersOf(req), { trustForwardedHost: trustsProxy(req) }));
590
+ return publicRequestUrl(local, headersOf(req), { trustForwardedHost: trustsProxy(req) });
590
591
  };
592
+ const resourceOf = (req, path, mcpPath) => brokerGate.resourceFor(publicMcpUrl(req, path, mcpPath));
591
593
  const cwd = options.cwd ?? process.cwd();
592
594
  const mounts = options.mounts;
593
595
  const brokerInfo = hostedResolverInfo(cfg.auth);
@@ -706,62 +708,73 @@ function aiReady(options = {}) {
706
708
  return;
707
709
  }
708
710
  if (path !== MCP_PATH && !path.startsWith(`${MCP_PATH}/`)) return next();
709
- if (disabled) {
710
- res.writeHead(503).end("agemno is disabled due to an internal error");
711
+ const handleMcp = () => {
712
+ if (disabled) {
713
+ res.writeHead(503).end("agemno is disabled due to an internal error");
714
+ return;
715
+ }
716
+ const authorized = gate ? gate.check(headersOf(req)).then(async (result) => {
717
+ if (result.ok) return { claims: result.claims };
718
+ await sendWebResponse(res, result.response);
719
+ return null;
720
+ }, (err) => {
721
+ console.warn("[agemno] oauth gate failed:", err);
722
+ if (!res.headersSent) res.writeHead(503).end("authorization check failed");
723
+ return null;
724
+ }) : brokerGate ? report(req, resourceOf(req, path, MCP_PATH)).then(async (verdict) => {
725
+ if (verdict === "open") return {};
726
+ const result = await brokerGate.check(headersOf(req), resourceOf(req, path, MCP_PATH));
727
+ if (result.ok) return {};
728
+ await sendWebResponse(res, result.response);
729
+ return null;
730
+ }).catch((err) => {
731
+ console.warn("[agemno] broker check failed:", err);
732
+ if (!res.headersSent) res.writeHead(503).end("authorization check failed");
733
+ return null;
734
+ }) : Promise.resolve({});
735
+ authorized.then((authContext) => {
736
+ if (!authContext) return;
737
+ handlerPromise ??= initialize(
738
+ req.app,
739
+ cfg,
740
+ cwd,
741
+ mounts,
742
+ { descriptions, openapi },
743
+ (surface) => logSurface(surface.routes, surface.docs.length, "updated: routes registered after startup")
744
+ );
745
+ return handlerPromise.then(
746
+ (handler) => (
747
+ // Tool calls made while handling this request reach the app with
748
+ // this caller's address and forwarding headers (see surface.ts).
749
+ runAsCaller(callerOf(req), () => handler(req, res, authContext))
750
+ )
751
+ );
752
+ }).catch((err) => {
753
+ disabled = true;
754
+ handlerPromise = null;
755
+ if (!warned) {
756
+ warned = true;
757
+ console.error(
758
+ formatFailed(
759
+ LOG_PREFIX,
760
+ errorMessage(err),
761
+ `${MCP_PATH} now answers 503 until the process restarts (your own routes are unaffected); full error below`
762
+ ),
763
+ err
764
+ );
765
+ }
766
+ if (!res.headersSent) res.writeHead(503).end("agemno failed to initialize");
767
+ });
768
+ };
769
+ if (brokerInfo) {
770
+ const mcpUrl = brokerInfo.resource ?? (brokerGate ? resourceOf(req, path, MCP_PATH) : publicMcpUrl(req, path, MCP_PATH));
771
+ void serveConnectInfo(new Request(mcpUrl, { method: req.method, headers: headersOf(req) }), brokerInfo, mcpUrl).then((info) => info ? sendWebResponse(res, info) : handleMcp()).catch((err) => {
772
+ console.warn("[agemno] connect-info failed:", err);
773
+ handleMcp();
774
+ });
711
775
  return;
712
776
  }
713
- const authorized = gate ? gate.check(headersOf(req)).then(async (result) => {
714
- if (result.ok) return { claims: result.claims };
715
- await sendWebResponse(res, result.response);
716
- return null;
717
- }, (err) => {
718
- console.warn("[agemno] oauth gate failed:", err);
719
- if (!res.headersSent) res.writeHead(503).end("authorization check failed");
720
- return null;
721
- }) : brokerGate ? report(req, resourceOf(req, path, MCP_PATH)).then(async (verdict) => {
722
- if (verdict === "open") return {};
723
- const result = await brokerGate.check(headersOf(req), resourceOf(req, path, MCP_PATH));
724
- if (result.ok) return {};
725
- await sendWebResponse(res, result.response);
726
- return null;
727
- }).catch((err) => {
728
- console.warn("[agemno] broker check failed:", err);
729
- if (!res.headersSent) res.writeHead(503).end("authorization check failed");
730
- return null;
731
- }) : Promise.resolve({});
732
- authorized.then((authContext) => {
733
- if (!authContext) return;
734
- handlerPromise ??= initialize(
735
- req.app,
736
- cfg,
737
- cwd,
738
- mounts,
739
- { descriptions, openapi },
740
- (surface) => logSurface(surface.routes, surface.docs.length, "updated: routes registered after startup")
741
- );
742
- return handlerPromise.then(
743
- (handler) => (
744
- // Tool calls made while handling this request reach the app with
745
- // this caller's address and forwarding headers (see surface.ts).
746
- runAsCaller(callerOf(req), () => handler(req, res, authContext))
747
- )
748
- );
749
- }).catch((err) => {
750
- disabled = true;
751
- handlerPromise = null;
752
- if (!warned) {
753
- warned = true;
754
- console.error(
755
- formatFailed(
756
- LOG_PREFIX,
757
- errorMessage(err),
758
- `${MCP_PATH} now answers 503 until the process restarts (your own routes are unaffected); full error below`
759
- ),
760
- err
761
- );
762
- }
763
- if (!res.headersSent) res.writeHead(503).end("agemno failed to initialize");
764
- });
777
+ handleMcp();
765
778
  };
766
779
  async function serveLoginPage(req, res, login) {
767
780
  const original = String(req.originalUrl ?? req.url);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exposify/express",
3
- "version": "0.3.15",
3
+ "version": "0.3.16",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",