@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
232 lines • 11.9 kB
TypeScript
import type { ApplicationService } from '@adonisjs/core/types';
import type { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
import type { Actor } from '../types.js';
/**
* Runtime context a {@link McpAuthFactory} thunk receives when the MCP provider builds the configured
* auth at boot. Carries the booted application so a driver can resolve a peer's service if it needs to.
*/
export interface McpAuthContext {
app: ApplicationService;
}
/**
* OAuth metadata the MCP server advertises at `/.well-known/oauth-protected-resource{path}` (RFC 9728)
* so an MCP client can discover how to obtain a token. Omitted → the server exposes no OAuth metadata
* endpoint and clients must authenticate another way (e.g. a static API key).
*/
export interface McpOAuthMetadata {
/** The OAuth 2.0 / OIDC authorization server (the authkit issuer, e.g. `http://localhost:3333/oidc`). */
issuer: string;
/** Authorization endpoint the MCP client redirects to (e.g. `${issuer}/auth`). */
authorizationEndpoint: string;
/** Token endpoint the MCP client exchanges the code at (e.g. `${issuer}/token`). */
tokenEndpoint: string;
/** Scopes supported by the authorization server. */
scopesSupported: string[];
/** Human label for the protected resource. */
resourceName: string;
}
/**
* The `extra` of an {@link McpAuthInfo}: the SDK's open record, with the acting {@link Actor} typed.
* A strategy of your own can carry more beside it (a session, the raw claims) without a cast, and
* the whole object is still an SDK `AuthInfo['extra']`.
*/
export interface McpAuthExtra {
actor: Actor;
[key: string]: unknown;
}
/**
* MCP `AuthInfo` with a typed acting {@link Actor} in `extra`. The MCP auth strategies
* (`authKitAuth`/`apiKeyAuth`) and the open-mode fallback all produce this shape, so consumers can
* read `extra.actor` without a runtime guard. It is assignable to the SDK's `AuthInfo`; going the
* other way — the `AuthInfo` the SDK hands a request handler — narrow with `isMcpAuthInfo`, or
* resolve the actor with `actorFromAuthInfo`, which throws when there is none.
*/
export interface McpAuthInfo extends Omit<AuthInfo, 'extra'> {
extra: McpAuthExtra;
}
/**
* A configured MCP auth strategy. `verify` validates a bearer token and resolves the acting {@link Actor}
* (attached to `extra.actor` as a typed {@link McpAuthInfo}); it MUST throw when the token is
* missing/invalid/expired. The same `AuthInfo` is surfaced to the `tools/list` / `tools/call` handlers
* as `extra.authInfo`, so the actor the tool registry gates against is exactly the one the auth
* strategy resolved — consumers can read it with `authInfo.extra.actor` or `actorFromAuthInfo`.
*/
export interface McpAuth {
/** OAuth metadata to advertise, resolved lazily (the authkit peer may not be bootable until first use). */
oauth?: McpOAuthMetadata | (() => McpOAuthMetadata | Promise<McpOAuthMetadata>);
/**
* Validate `token` and resolve the acting actor as a typed `McpAuthInfo`. Throws on rejection:
* {@link McpAuthError} for a deliberate refusal (its `status` and message reach the client), any
* other `Error` for a token this strategy does not recognize (→ `401`).
*/
verify(token: string, context?: McpVerifyContext): Promise<McpAuthInfo>;
}
/** What the provider knows about the request a token is verified for. */
export interface McpVerifyContext {
/**
* The protected resource (RFC 9728 `resource`) being called, e.g. `https://app.example.com/mcp` —
* what a token's audience must name (MCP Authorization spec, RFC 8707).
*/
resource: string;
}
/**
* A typed refusal from an MCP auth strategy: the token was recognized and deliberately refused (a
* client that is not allowed on this endpoint, an account that left the tenant, a revoked grant).
*
* Throw it from `verify` — or from the `toActor` of `authKitAuth()` / `apiKeyAuth()` — and the
* provider answers with its `status` and message, plus the matching `WWW-Authenticate` challenge:
* `401` → `error="invalid_token"`, `403` → `error="insufficient_scope"`.
*
* Inside {@link anyOf} it is also a stop signal: a strategy that throws `McpAuthError` has claimed
* the token, so the remaining strategies are not tried. Any other error means "not my token" and
* the next strategy gets its turn.
*
* ```ts
* authKitAuth({
* toActor: ({ accountId, clientId }) => {
* if (clientId === 'web') throw new McpAuthError('token was not issued for MCP')
* return { id: accountId }
* },
* })
* ```
*/
export declare class McpAuthError extends Error {
/** HTTP status of the refusal: `401` (the token does not authenticate) or `403` (it does, but may not do this). */
readonly status: 401 | 403;
/**
* The `WWW-Authenticate` error code, when not the status' default (`invalid_token` for 401,
* `insufficient_scope` for 403) — e.g. Poppy's `sign_in_required` (§4.3).
*/
readonly error?: McpChallengeError;
constructor(message: string, options?: {
status?: 401 | 403;
error?: McpChallengeError;
cause?: unknown;
});
}
/** Error codes the MCP endpoint can put in its `WWW-Authenticate` challenge. */
export type McpChallengeError = 'invalid_token' | 'insufficient_scope' | 'sign_in_required';
/**
* A configured MCP auth strategy, as a lazy thunk the provider calls at boot. Each factory lazily
* imports / resolves its peer inside the thunk, so nothing loads until the strategy is actually selected.
*/
export type McpAuthFactory = (ctx: McpAuthContext) => McpAuth | Promise<McpAuth>;
/**
* Resolve a `McpAuth` config value (a ready instance or a lazy factory) at provider boot.
*/
export declare function resolveMcpAuth(auth: McpAuth | McpAuthFactory, ctx: McpAuthContext): Promise<McpAuth>;
/** The `gty` authkit-server gives the access tokens it mints for a Poppy session (§6, MCP). */
export declare const POPPY_GRANT_TYPE = "poppy";
/**
* The organization authkit bound to a grant at consent time (the active org of the session that
* approved it). Stored on the grant as `activeOrg`.
*/
export interface AuthKitActiveOrg {
orgId: string;
orgSlug?: string;
orgRole?: string;
}
/**
* The oidc-provider `Grant` behind an access token — the authorization the user gave to a client.
* Duck-typed: only the fields this package reads are named; the rest of the stored grant is there too.
*/
export interface AuthKitGrant {
accountId?: string;
clientId?: string;
/** Set by authkit at consent when the session had an active organization. */
activeOrg?: unknown;
[key: string]: unknown;
}
/** What `authKitAuth()` hands `toActor` for a verified access token. */
export interface AuthKitActorInfo {
/** The account the token was issued to. */
accountId: string;
/** The token's granted scopes. */
scopes: string[];
/** The client the token was issued to — refuse clients that should not reach MCP by throwing {@link McpAuthError}. */
clientId: string | undefined;
/** The grant id the token belongs to, when the provider tracks one. */
grantId: string | undefined;
/** The grant itself (loaded per request, so a revoked grant stops the token right away). */
grant: AuthKitGrant | undefined;
/** The organization authkit bound to the grant at consent — the tenant the user authorized. */
activeOrg: AuthKitActiveOrg | undefined;
/** Extra claims stored on the access token (`extraTokenClaims`), if any. */
extra: Record<string, unknown> | undefined;
/**
* The token belongs to a **personal agent** — authkit minted it for a Personal Agent Protocol
* (Poppy) session (`gty: 'poppy'`), not for an MCP client the user logged into. Whatever `toActor`
* returns, the actor then also gets `personal_agent` and one `scope:<id>` role per granted scope,
* so the personal agent gate applies: only the tools of its scopes run.
*/
personalAgent: boolean;
}
/**
* How `authKitAuth()` maps an access token to the MCP actor. Defaults to the account id. Throw
* {@link McpAuthError} to refuse the token (e.g. a client that is not allowed on this endpoint).
*/
export type AuthKitActorResolver = (info: AuthKitActorInfo) => Actor | Promise<Actor>;
/** Options for {@link authKitAuth}. */
export interface AuthKitMcpAuthOptions {
/** Scopes the authorization server advertises. Defaults to the standard authkit scopes. */
scopes?: string[];
/** Map a validated access token to the acting {@link Actor}. Defaults to `{ id: accountId }`. */
toActor?: AuthKitActorResolver;
/** Human label for the protected resource. Defaults to `'Agent MCP Server'`. */
resourceName?: string;
/**
* Which tokens this endpoint accepts, by audience:
* - `'resource'` (default) — only a token issued for THIS server: its `aud` (RFC 8707) must be
* the MCP URL. A token for another resource, or one with no audience (a web login's), is
* refused — the MCP Authorization spec forbids accepting tokens not meant for the server.
* - `'any'` — skip the check (tokens from clients that do not send `resource`).
*/
audience?: 'resource' | 'any';
}
/**
* Authenticate MCP clients with `@adonis-agora/authkit-server`: verifies the bearer token against the
* authkit OIDC provider's stored access tokens (`provider.AccessToken.find`) and advertises OAuth
* metadata derived from the issuer so MCP clients can run the full OAuth login flow.
*
* The authkit peer is resolved lazily — the `verify`/`oauth` accessors touch the container only when
* a request actually arrives (by then the app is fully booted and the authkit keystore/encryption
* services are ready). `@adonis-agora/authkit-server` stays an optional dependency. Throws a clear
* error at first use if the binding isn't resolvable (authkit not installed / provider not registered).
*/
export declare function authKitAuth(options?: AuthKitMcpAuthOptions): McpAuthFactory;
/** Resolve the MCP actor from a validated API key. Defaults to `{ id: apiKey }`. */
export type ApiKeyActorResolver = (apiKey: string) => Actor | Promise<Actor>;
/** Options for {@link apiKeyAuth}. */
export interface ApiKeyMcpAuthOptions {
/** Static API keys clients present in `Authorization: Bearer <key>`. */
apiKeys: string[];
/** Map a validated API key to the acting {@link Actor}. Defaults to `{ id: apiKey }`. */
toActor?: ApiKeyActorResolver;
}
/**
* Authenticate MCP clients with a static list of API keys (no OAuth metadata). Intended for trusted
* gateway / machine-to-machine integrations where the host proxies auth and this server just
* checks the key. Keys are compared with a constant-time compare to resist timing attacks.
*/
export declare function apiKeyAuth(options: ApiKeyMcpAuthOptions): McpAuthFactory;
/**
* Accept more than one kind of bearer token on the same MCP endpoint — e.g. OAuth access tokens from
* MCP clients that logged in through the browser AND personal access tokens for machine integrations:
*
* ```ts
* auth: anyOf(
* authKitAuth({ toActor }),
* { verify: verifyPersonalAccessToken },
* )
* ```
*
* Strategies are tried in order and the first that verifies the token wins. A strategy that throws a
* plain `Error` passes ("not my token"); one that throws {@link McpAuthError} has claimed the token and
* refused it, so the chain stops there with that refusal. When every strategy passes, the last error
* is rethrown.
*
* OAuth metadata comes from the first strategy that exposes it, so the RFC 9728 document and the
* `WWW-Authenticate` challenge still point MCP clients at the login flow.
*/
export declare function anyOf(...strategies: Array<McpAuth | McpAuthFactory>): McpAuthFactory;
//# sourceMappingURL=auth.d.ts.map