@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
85 lines • 4.25 kB
JavaScript
/**
* The OAuth discovery surface of the MCP server (MCP Authorization spec, RFC 9728, RFC 6750 §3): the
* protected-resource metadata document, its well-known URL, and the `WWW-Authenticate` challenge a
* `401`/`403` carries so an MCP client knows where to start the login.
*
* Pure functions over strings — the provider decides which origin to feed them (the configured
* `publicUrl`, or the request's own protocol and `Host` as a fallback).
*/
/** Normalize a route prefix the way the provider mounts it: no leading/trailing slashes. */
export function normalizeMcpPath(path) {
return (path ?? 'mcp').replace(/^\/+|\/+$/g, '');
}
/**
* Validate `publicUrl` from `config/mcp.ts` and reduce it to its origin (`https://app.example.com`).
* Only an origin is meaningful: the MCP endpoint and its well-known document are mounted at fixed
* paths on this app, so a path, query, or fragment would advertise URLs the app does not serve.
*/
export function publicOrigin(publicUrl) {
let url;
try {
url = new URL(publicUrl);
}
catch {
throw new Error(`[@adonis-agora/agent] \`publicUrl\` in config/mcp.ts is not an absolute URL: ${JSON.stringify(publicUrl)}`);
}
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
throw new Error(`[@adonis-agora/agent] \`publicUrl\` in config/mcp.ts must be http(s), got ${JSON.stringify(publicUrl)}`);
}
if (url.pathname !== '/' || url.search !== '' || url.hash !== '') {
throw new Error(`[@adonis-agora/agent] \`publicUrl\` in config/mcp.ts must be an origin only (e.g. "https://app.example.com"), got ${JSON.stringify(publicUrl)}`);
}
return url.origin;
}
/** The protected resource (RFC 9728 `resource`): the URL an MCP client points at, e.g. `https://app.example.com/mcp`. */
export function mcpResourceUrl(origin, path) {
return `${origin}/${normalizeMcpPath(path)}`;
}
/** Where the protected-resource metadata lives (RFC 9728 §3.1): the well-known segment goes between host and path. */
export function protectedResourceMetadataUrl(origin, path) {
return `${origin}/.well-known/oauth-protected-resource/${normalizeMcpPath(path)}`;
}
/**
* The Poppy issuer `@adonis-agora/authkit-server` announces in `Symbol.for('@adonis-agora/poppy:issuer')`
* when Personal Agent Protocol is on — Poppy §6 requires a listed MCP server's `authorization_servers`
* to include it, so a Poppy Session Token's issuer is one this server names.
*/
export function announcedPoppyIssuer() {
const value = globalThis[Symbol.for('@adonis-agora/poppy:issuer')];
return typeof value === 'string' && value !== '' ? value : undefined;
}
/**
* Build the RFC 9728 document for the MCP endpoint at `origin` + `path`. Its `authorization_servers`
* is the configured issuer, plus the Poppy issuer when one is announced (and different).
*/
export function protectedResourceMetadata(meta, origin, path) {
const poppyIssuer = announcedPoppyIssuer();
return {
resource: mcpResourceUrl(origin, path),
authorization_servers: [...new Set([meta.issuer, ...(poppyIssuer ? [poppyIssuer] : [])])],
scopes_supported: meta.scopesSupported,
bearer_methods_supported: ['header'],
resource_name: meta.resourceName,
};
}
/**
* The `WWW-Authenticate` value for a `401`/`403` from the MCP endpoint — e.g.
* `Bearer error="invalid_token", resource_metadata="https://app.example.com/.well-known/oauth-protected-resource/mcp"`.
* `resource_metadata` is what makes an MCP client discover the authorization server and open the login.
*/
export function wwwAuthenticateChallenge(options = {}) {
const params = [];
if (options.error !== undefined)
params.push(`error="${options.error}"`);
if (options.scope !== undefined)
params.push(`scope="${quote(options.scope)}"`);
if (options.resourceMetadataUrl !== undefined) {
params.push(`resource_metadata="${quote(options.resourceMetadataUrl)}"`);
}
return params.length === 0 ? 'Bearer' : `Bearer ${params.join(', ')}`;
}
/** Escape a value for an RFC 7230 quoted-string. */
function quote(value) {
return value.replace(/[\\"]/g, (char) => `\\${char}`);
}
//# sourceMappingURL=discovery.js.map