@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 +12 -0
- package/dist/index.cjs +68 -56
- package/dist/index.js +69 -56
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
|
|
702
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
710
|
-
|
|
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
|
-
|
|
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);
|