UNPKG

@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
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