@dinko/adonis-mcp
adonis-oauth-mcp
Monorepo para dos paquetes AdonisJS. Versionan y publican juntos, por lo que los cambios en el contrato de registro de recursos nunca necesitan coordinación entre repositorios.
Package | Posee |
| Servidor de autorización OAuth 2.1: token / approve / deny, validación de redirect-URI, almacenamiento de códigos de autorización, metadatos del servidor de autorización y un endpoint genérico de metadatos de recursos protegidos impulsado por un registro de recursos. No sabe nada de MCP. |
| Servidor MCP: manejador de peticiones, controlador, contrato de herramientas, middleware de autenticación. Se registra como recurso protegido OAuth, declarando su URL de recurso, scopes, clientes, |
La dependencia es unidireccional: mcp → oauth. Nada en oauth puede importar desde mcp.
Estructura
Cada paquete sigue la convención de paquetes de AdonisJS:
index.ts re-exports `configure` and `stubsRoot` (what `node ace configure` imports)
configure.ts the configure hook, driving codemods and stubs
stubs/ .stub templates rendered into the target app
src/ runtime code the app imports
providers/ service providers registered by the configure hook
services/ container services, for code that cannot use dependency injectionRelated MCP server: mcpauth
Desarrollo
npm install # links the workspaces
npm run build # tsc + copy stubs, per package
npm run typecheck
npm test # runs against build/, so build firstDurante el desarrollo, instala en una aplicación desde este checkout (npm link, file: o una dependencia git) en lugar de desde el registro.
@dinko/adonis-oauth
Un servidor de autorización OAuth 2.1 con PKCE, para aplicaciones AdonisJS que necesitan entregar tokens de acceso a clientes de terceros.
El paquete es dueño del protocolo. La aplicación es dueña de tres cosas que no puede delegar: la pantalla de consentimiento, qué token emitir y las rutas.
npm i @dinko/adonis-oauth
node ace configure @dinko/adonis-oauthLa configuración genera tres archivos y nunca sobrescribe uno existente:
File | Qué hacer con él |
| Declara tus recursos, sus clientes y |
| Ajusta la columna |
| Tuyo a partir de aquí: delega en el paquete y es donde añades cualquier cosa que no cubra. |
Rutas
No se registran automáticamente: dónde viven y qué middleware las protege es tu decisión. Añádelas a start/routes.ts:
router.get('.well-known/oauth-authorization-server', [OauthController, 'getAuthorizationServer'])
router.get('.well-known/oauth-protected-resource/:resource', [OauthController, 'getProtectedResource'])
router
.group(() => {
router.post('token', [OauthController, 'token'])
router
.group(() => {
router.post('authorize/approve', [OauthController, 'approveAuthorization'])
router.post('authorize/deny', [OauthController, 'denyAuthorization'])
})
.use(middleware.auth())
})
.prefix('oauth')Approve y deny deben estar autenticados: el código de autorización está vinculado al usuario que concede el acceso. El endpoint de token es público por especificación y es donde se canjean los códigos de autorización, por lo que es un buen lugar para un throttle.
Devolviendo la redirección
Approve y deny responden 200 { redirect_to } por defecto, y la pantalla de consentimiento navega por sí misma:
window.location.assign(response.redirect_to)Eso es lo que necesita una pantalla que envía su decisión con fetch o axios. Un XHR sigue un 302 reemitiendo la petición, por lo que la página nunca navega: el usuario permanece en la pantalla de consentimiento mientras la petición llega cross-origin al callback del cliente y falla CORS.
Establece redirectMode: 'http' cuando la pantalla de consentimiento es un formulario HTML simple. Ahí el navegador navega el documento, por lo que sigue el 302 de forma nativa y el usuario aterriza en el cliente.
Emisión de tokens
El tipo de token depende del recurso al que se accede, por lo que esa decisión vive con cada recurso en lugar de en el controlador. Una vez que el paquete ha validado la petición, consumido el código de autorización y verificado el verificador PKCE, llama a:
issueToken: async ({ userId, scopes, client, resource, ctx }) => {
const user = await User.find(userId)
if (!user) return null // rejects the exchange with invalid_grant
const expiresIn = 30 * 24 * 60 * 60
const token = await User.accessTokens.create(user, scopes, {
name: `oauth:${client.id}`,
expiresIn,
})
return { accessToken: token.value!.release(), expiresIn }
}userId es lo que se almacenó con el código de autorización: el paquete no tiene conocimiento de tu modelo de usuario y nunca lo carga.
La pantalla de consentimiento
La página GET /oauth/authorize es tuya — Edge, Inertia o un front end separado. El paquete solo valida la petición detrás de ella:
const validation = server.validateAuthorizationRequest(request.qs())
if (!validation.valid) {
return view.render('oauth/authorize', { error: validation.error })
}
return view.render('oauth/authorize', {
client: validation.client,
requestedScopes: validation.scopes,
authorizationFields: validation.fields, // post these back to approve
})Opcional: las aplicaciones que renderizan la pantalla en otro lugar pueden omitirlo, ya que approve y deny validan la petición de nuevo por su cuenta.
Configuración
export default defineConfig({
issuer: env.get('APP_URL'),
authorizationEndpoint: `${env.get('APP_URL')}/oauth/authorize`,
tokenEndpoint: `${env.get('APP_URL')}/oauth/token`,
// optional
redirectMode: 'json', // or 'http'
tokenEndpointAuthMethods: ['none'],
authorizationCodeTtlSeconds: 10 * 60,
authorizationCodesTable: 'oauth_authorization_codes',
authenticatedUserId: (ctx) => ctx.auth.user?.id, // defaults to this
resources: [mcpResource],
})Cada recurso declara:
Field | |
| Slug utilizado en |
| Indicador de recurso canónico que los clientes envían como parámetro |
| Nombre legible por humanos, anunciado a través de discovery. |
| Todos los scopes que el recurso entiende. |
|
|
| Genera el token de acceso. |
Las redirect URIs de loopback (http://localhost/callback) coinciden en cualquier puerto, según RFC 8252, y redirectUriPatterns permite un único segmento :param para clientes cuyo callback lleva un id.
This server cannot be deployed
Maintenance
Related MCP Connectors
One OAuth connector for your team's remote MCP servers, with Tool Packs, guardrails and logs.
Self-hosted federated MCP gateway: one OAuth 2.1 MCP server in front of N apps, user-level scopes.
MCP server for verifying EUDI/Talao wallet data via OIDC4VP (pull) for AI agents.
Hosted MCP server with managed OAuth for 15+ toolkits: Google Workspace, Fitbit, Oura, Kalshi, etc.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceA complete OAuth 2.1 server implementation for FastMCP with PKCE support, enabling secure authentication and authorization flows. Provides authorization code exchange, token management, and refresh capabilities for building authenticated MCP applications.-
- AlicenseNot gradedqualityFmaintenanceDrop-in OAuth 2.1 + Dynamic Client Registration for MCP servers, providing authentication middleware and token verification.12 npm1MIT
- AlicenseNot gradedqualityDmaintenanceImplements an MCP server with OAuth 2.1 Protected Resource Metadata, enabling token-based authentication for MCP tools like ping.MIT
- AlicenseBqualityDmaintenanceA production-grade Model Context Protocol (MCP) resource server with OAuth 2.1 authorization, provider-agnostic and supporting multiple MCP specification versions.18MIT