AdonisJS MCP (@jrmc/adonis-mcp)
Build Model Context Protocol servers inside AdonisJS applications. Tools, resources, and prompts are classes auto-discovered from app/mcp/{tools,resources,prompts}/.
Not installed yet? See references/setup.md — installation, config/mcp.ts, route registration, CSRF exclusion, stateless middleware, legacy sessions, stdio transport.
Auth & permissions? See references/authentication.md — auth middleware, Bouncer abilities/policies, type augmentation.
Tests & debugging? See references/testing.md — Japa integration tests with FakeTransport, MCP Inspector.
Critical conventions
- File naming is mandatory for auto-discovery: files must end with
_tool.ts,_resource.ts, or_prompt.tsand live underapp/mcp/tools/,app/mcp/resources/, orapp/mcp/prompts/(path configurable viadirectories.mcpinadonisrc.ts). A misnamed file is silently ignored. - Always scaffold with ace (uses the right stub, including the VineJS variant when the app uses VineJS):
node ace make:mcp-tool my_tool node ace make:mcp-resource my_resource node ace make:mcp-prompt my_prompt - Schemas are JSON Schema and must be returned
as Schemafromschema(). Zod (z.toJSONSchema(zodSchema, { io: "input" })) or VineJS ≥ v4 (vine.create(vineSchema).toJSONSchema()) can generate them. - Return multiple contents as an array. There is no
response.send()method:return [response.text('First'), response.text('Second')]. - Always
awaitbouncer calls — forgettingawaitturns an authorization error into an unhandled rejection instead of a JSON-RPC error.
Tool
import type { ToolContext } from '@jrmc/adonis-mcp/types/context'
import type { BaseSchema } from '@jrmc/adonis-mcp/types/method'
import { Tool } from '@jrmc/adonis-mcp'
type Schema = BaseSchema<{
title: { type: "string" }
url: { type: "string" }
}>
export default class AddBookmarkTool extends Tool<Schema> {
name = 'create_bookmark' // required, unique
title = 'Create Bookmark' // optional
description = 'Create a new bookmark'
async handle({ args, response, auth, bouncer, request }: ToolContext<Schema>) {
return response.text(JSON.stringify({ title: args?.title }))
}
schema() {
return {
type: "object",
properties: {
title: { type: "string", description: "Bookmark title" },
url: { type: "string", description: "Bookmark URL" }
},
required: ["title", "url"]
} as Schema
}
}
Omit schema() and the <Schema> generic entirely for tools without input.
Tool annotations (from @jrmc/adonis-mcp/tool_annotations)
Class decorators, combinable: @isReadOnly(), @isOpenWorld(), @isDestructive(), @isIdempotent(). Each accepts an optional boolean (@isReadOnly(false)).
Resource
import type { ResourceContext } from '@jrmc/adonis-mcp/types/context'
import { Resource } from '@jrmc/adonis-mcp'
type Args = { id: string }
export default class UserDocumentResource extends Resource<Args> {
name = 'user_document'
uri = 'document:///{id}' // required, unique; RFC 6570 template
mimeType = 'text/plain'
title = 'User Document'
description = 'Access user documents by ID'
async handle({ args, response }: ResourceContext<Args>) {
const content = await loadDocument(args?.id)
this.size = content.length // set size when known
return response.text(content) // or response.blob(buffer) for binary
}
}
- URI template operators:
{name},{/name},{?name},{&name},{#name},{+name},{.name}. Template variables arrive typed inargs. - Resource annotations (from
@jrmc/adonis-mcp/annotations):@priority(0.0–1.0),@audience(Role.USER | Role.ASSISTANT | [both])(Role from@jrmc/adonis-mcp/enums/role),@lastModified('ISO-8601').
Prompt
import type { PromptContext } from '@jrmc/adonis-mcp/types/context'
import type { BaseSchema } from '@jrmc/adonis-mcp/types/method'
import { Prompt } from '@jrmc/adonis-mcp'
type Schema = BaseSchema<{
code: { type: "string" }
language: { type: "string" }
}>
export default class CodeReviewPrompt extends Prompt<Schema> {
name = 'code_review'
title = 'Code Review'
description = 'Review code and provide feedback'
async handle({ args, response }: PromptContext<Schema>) {
return [
response.text(`Please review this ${args?.language} code:`),
response.text(args?.code),
response.embeddedResource('file:///guidelines/coding-standards.md'),
]
}
schema() { /* same JSON Schema pattern as tools */ }
}
Responses
| Method | Handlers | Use |
|---|---|---|
response.text(string) |
Tool, resource, prompt | Plain text |
response.image(base64, mime) / response.audio(base64, mime) |
Tool, prompt | Media |
response.structured(object) |
Tool | Structured JSON |
response.blob(stringOrBuffer) |
Resource | Text or a Buffer, encoded to base64 by the package |
response.resourceLink(uri) |
Tool | Link to a registered resource |
response.embeddedResource(uri) |
Tool, prompt | Embed a registered resource |
response.error(message) |
Tool, resource | Tool error or JSON-RPC resource error |
Return an array directly for multiple tool or prompt contents. Text, image, audio, blob, resource
link, embedded resource, and error content support .withMeta({ ... }); structured content does not.
Errors
For a specific JSON-RPC error code or extra data, throw instead of response.error():
import JsonRpcException from '@jrmc/adonis-mcp/exceptions'
import { ErrorCode } from '@jrmc/adonis-mcp/enums/error'
throw new JsonRpcException('Not found', ErrorCode.InvalidParams, request.id, { resource: 'user' })
Available ErrorCode values include ConnectionClosed (-32000), RequestTimeout (-32001),
HeaderMismatch (-32020), MissingRequiredClientCapability (-32021),
UnsupportedProtocolVersion (-32022), ParseError (-32700), InvalidRequest (-32600),
MethodNotFound (-32601), InvalidParams (-32602), and InternalError (-32603). Exceptions work
in tools, resources, and prompts (request.id comes from the context).
Protocol-aware handlers
Handlers can inspect protocolEra, protocolVersion, clientCapabilities, clientInfo, and
logLevel from their context. Modern 2026-07-28 requests are stateless; legacy clients use
initialize and MCP-Session-Id. Read references/setup.md before changing
the generated middleware or cache policy.
A primitive tool argument can require a mirrored HTTP header:
region: { 'type': 'string', 'x-mcp-header': 'Region' }
For modern tools/call, the body value must match Mcp-Param-Region. Restrict this annotation to
primitive string, integer, or boolean properties.
VineJS validation in handlers
If the VineJS provider is registered (@jrmc/adonis-mcp/vinejs_provider in adonisrc.ts), reuse your app's validators — validation errors become JSON-RPC errors automatically, no try/catch:
async handle({ request, response }: ToolContext) {
const payload = await request.validateUsing(ArticleValidator)
// ...
}
Completions (autocomplete for prompt args / resource template variables)
Requires completions: true in config/mcp.ts. Implement complete() on the prompt or resource:
import type { CompleteContext } from '@jrmc/adonis-mcp/types/context'
async complete({ args, response }: CompleteContext<Args>) {
if (args?.language !== undefined) {
return response.complete({ values: ['typescript', 'python'] }) // + optional hasMore, total
}
return response.complete({ values: [] })
}
Dependency injection
Standard AdonisJS @inject() works on constructors and handle() methods of tools/resources/prompts.
Events
mcp:request and mcp:response are emitted on the AdonisJS emitter (JsonRpcRequest / JsonRpcResponse from @jrmc/adonis-mcp/types/jsonrpc).