Resources
Resources represent data or content that AI models can access from your application. They provide a structured way to expose files, documents, database records, or any other content.
Creating Resources
To create a new resource, use the Ace command:
node ace make:mcp-resource my_resourceThis command will create a file in app/mcp/resources/my_resource.ts with a base template:
import type { ResourceContext } from '@jrmc/adonis-mcp/types/context'
import { Resource } from '@jrmc/adonis-mcp'
export default class MyResourceResource extends Resource {
name = 'example.txt'
uri = 'file:///example.txt'
mimeType = 'text/plain'
title = 'Resource title'
description = 'Resource description'
size = 0
async handle({ response }: ResourceContext) {
this.size = 1000
return response.text('Hello World')
}
}Resource Properties
Resources have the following properties:
- uri (required): The unique identifier for the resource (must be unique across all resources)
- name (optional): The name of the resource
- mimeType (optional): The MIME type of the resource (e.g.,
text/plain,application/json) - title (optional): A human-readable title
- description (optional): A description of the resource
- size (optional): The size of the resource in bytes
Resource Templates
Resources support URI templates (RFC 6570) to create dynamic resources. This allows you to define resources with variable parts in their URIs.
Basic Template
import type { ResourceContext } from '@jrmc/adonis-mcp/types/context'
import { Resource } from '@jrmc/adonis-mcp'
type Args = {
name: string
}
export default class DynamicFileResource extends Resource<Args> {
name = 'dynamic_file'
uri = 'file:///{name}.txt'
mimeType = 'text/plain'
title = 'Dynamic file'
description = 'Access files dynamically by name'
async handle({ args, response }: ResourceContext<Args>) {
const content = await readFile(args?.name)
this.size = content.length
return response.text(content)
}
}When a client requests file:///robots.txt, the template file:///{name}.txt will match and extract name: "robots" as an argument.
Template Operators
URI templates support various operators:
{name}- Simple variable substitution{/name}- Path segment{?name}- Query parameter{&name}- Additional query parameter{#name}- Fragment identifier{+name}- Reserved characters allowed{.name}- Dot-prefixed segment
Example with multiple variables:
type Args = {
directory: string
filename: string
}
export default class NestedFileResource extends Resource<Args> {
uri = 'file:///{directory}/{filename}.txt'
async handle({ args, response }: ResourceContext<Args>) {
const path = `${args?.directory}/${args?.filename}.txt`
const content = await readFile(path)
return response.text(content)
}
}For more information, see RFC 6570.
Resource URI and MIME Type
URI Format
The URI must be a valid URI string and should be unique across all resources. Common URI schemes:
file:///- For file system resourceshttp://orhttps://- For web resourcesdata://- For data URIs- Custom schemes - You can define your own URI schemes
Examples:
// File resource
uri = 'file:///documents/readme.txt'
// Web resource
uri = 'https://api.example.com/data'
// Custom scheme
uri = 'myapp://users/profile'MIME Types
MIME types help AI clients understand the content format. Common MIME types:
text/plain- Plain texttext/html- HTML contenttext/markdown- Markdown contentapplication/json- JSON dataapplication/pdf- PDF documentsimage/png,image/jpeg- Imagesaudio/mpeg- Audio files
export default class JsonDataResource extends Resource {
uri = 'data://users'
mimeType = 'application/json'
async handle({ response }: ResourceContext) {
const users = await User.all()
return response.text(JSON.stringify(users))
}
}Resource Request
The handle method processes resource requests and returns the content. It receives a ResourceContext with:
- args: Extracted variables from URI templates (if applicable)
- response: Response builder with formatting methods
- auth: AdonisJS auth instance (if authentication middleware is applied)
- bouncer: AdonisJS bouncer instance (if available)
Simple Resource
async handle({ response }: ResourceContext) {
const content = 'Hello, World!'
this.size = content.length
return response.text(content)
}Dynamic Resource with Arguments
async handle({ args, response }: ResourceContext<Args>) {
const file = await File.findBy('name', args?.name)
if (!file) {
throw new Error('File not found')
}
this.size = file.size
return response.text(file.content)
}Authenticated Resource
async handle({ auth, response }: ResourceContext) {
const user = auth?.user
if (!user) {
throw new Error('Authentication required')
}
const documents = await user.related('documents').query()
const content = JSON.stringify(documents)
this.size = content.length
return response.text(content)
}Resource with Authorization
async handle({ bouncer, response }: ResourceContext) {
await bouncer?.authorize('viewSecretDocuments')
const content = await getSecretContent()
this.size = content.length
return response.text(content)
}Resource Responses
Resources can return two types of content:
Text Content
For text-based resources (documents, JSON, XML, etc.):
async handle({ response }: ResourceContext) {
const content = await fs.readFile('path/to/file.txt', 'utf-8')
return response.text(content)
}Binary Content (Blob)
For binary resources (images, PDFs, etc.), encode as base64:
async handle({ response }: ResourceContext) {
const content = await fs.readFile('path/to/image.png', 'base64')
return response.blob(content)
}Response Metadata
Add metadata to responses using withMeta():
async handle({ response }: ResourceContext) {
const content = await getContent()
return response.text(content).withMeta({
lastModified: new Date().toISOString(),
source: 'database',
cached: false
})
}Resource Annotations
Annotations provide additional metadata about your resources to help AI clients understand their characteristics.
@priority()
Specifies the importance of a resource as a number between 0.0 and 1.0:
import { Resource } from '@jrmc/adonis-mcp'
import { priority } from '@jrmc/adonis-mcp/annotations'
@priority(0.9)
export default class ImportantDocResource extends Resource {
name = 'important_doc.txt'
uri = 'file:///important_doc.txt'
async handle({ response }: ResourceContext) {
return response.text('Critical documentation content')
}
}Higher priority values (closer to 1.0) indicate more important resources that AI clients should prioritize.
@audience()
Specifies the intended audience for a resource (user, assistant, or both):
import { Resource } from '@jrmc/adonis-mcp'
import { audience } from '@jrmc/adonis-mcp/annotations'
import Role from '@jrmc/adonis-mcp/enums/role'
@audience(Role.USER)
export default class UserManualResource extends Resource {
name = 'user_manual.txt'
uri = 'file:///user_manual.txt'
async handle({ response }: ResourceContext) {
return response.text('User manual content')
}
}You can specify multiple audiences:
@audience([Role.USER, Role.ASSISTANT])
export default class SharedDocResource extends Resource {
// ...
}Available roles:
Role.USER- Content intended for end usersRole.ASSISTANT- Content intended for AI assistants
@lastModified()
Indicates when a resource was last updated (ISO 8601 timestamp):
import { Resource } from '@jrmc/adonis-mcp'
import { lastModified } from '@jrmc/adonis-mcp/annotations'
@lastModified('2024-12-12T10:00:00Z')
export default class DocumentResource extends Resource {
name = 'document.txt'
uri = 'file:///document.txt'
async handle({ response }: ResourceContext) {
return response.text('Document content')
}
}You can also set this dynamically:
export default class DynamicDocResource extends Resource {
async handle({ response }: ResourceContext) {
const doc = await Document.first()
this.setAnnotation('lastModified', doc.updatedAt.toISOString())
return response.text(doc.content)
}
}Combining Resource Annotations
You can use multiple annotations on the same resource:
import { Resource } from '@jrmc/adonis-mcp'
import { priority, audience, lastModified } from '@jrmc/adonis-mcp/annotations'
import Role from '@jrmc/adonis-mcp/enums/role'
@priority(0.8)
@audience([Role.USER, Role.ASSISTANT])
@lastModified('2024-12-12T10:00:00Z')
export default class ApiDocResource extends Resource {
name = 'api_docs.txt'
uri = 'file:///api_docs.txt'
async handle({ response }: ResourceContext) {
return response.text('API documentation content')
}
}Complete Example
Here is a complete example of a resource that provides access to user documents:
import type { ResourceContext } from '@jrmc/adonis-mcp/types/context'
import { Resource } from '@jrmc/adonis-mcp'
import { priority, audience } from '@jrmc/adonis-mcp/annotations'
import Role from '@jrmc/adonis-mcp/enums/role'
import Document from '#models/document'
type Args = {
id: string
}
@priority(0.7)
@audience([Role.USER, Role.ASSISTANT])
export default class UserDocumentResource extends Resource<Args> {
name = 'user_document'
uri = 'document:///{id}'
mimeType = 'text/plain'
title = 'User Document'
description = 'Access user documents by ID'
async handle({ args, auth, response }: ResourceContext<Args>) {
const user = auth?.user
if (!user) {
throw new Error('Authentication required')
}
const document = await Document.query()
.where('id', args?.id)
.where('userId', user.id)
.firstOrFail()
this.size = document.content.length
return response.text(document.content).withMeta({
documentId: document.id,
createdAt: document.createdAt.toISOString(),
updatedAt: document.updatedAt.toISOString()
})
}
}Completions
Resources with URI templates can provide completions for their path parameters to help users fill in values interactively.
First, enable completions in your config/mcp.ts:
import { defineConfig } from '@jrmc/adonis-mcp'
export default defineConfig({
name: 'adonis-mcp-server',
version: '1.0.0',
completions: true, // Enable completions
})Then implement the complete() method:
import type { ResourceContext, CompleteContext } from '@jrmc/adonis-mcp/types/context'
import { Resource } from '@jrmc/adonis-mcp'
type Args = {
directory: string
name: string
}
export default class ConfigFileResource extends Resource<Args> {
name = 'config_file'
uri = 'file://{directory}/{name}.txt'
mimeType = 'text/plain'
async handle({ args, response }: ResourceContext<Args>) {
const content = await readConfigFile(args?.directory, args?.name)
return response.text(content)
}
async complete({ args, response }: CompleteContext<Args>) {
// Provide suggestions based on what's being completed
if (args?.name !== undefined) {
return response.complete({
values: ['config', 'settings', 'environment', 'database']
})
}
if (args?.directory !== undefined) {
return response.complete({
values: ['production', 'staging', 'development']
})
}
return response.complete({ values: [] })
}
}