MCP Resources

Resources in MCP represent accessible data and content within Lokus. They provide a standardized way for AI assistants to discover, read, and subscribe to application data.

Overview

An MCP resource is any piece of data that can be:

  • Discovered: Listed through the resources/list method
  • Read: Accessed through the resources/read method
  • Subscribed: Monitored for changes (optional)
  • Identified: Referenced by a unique URI

Resource Structure

Basic Resource Definition

interface MCPResource {
  uri: string                      // Unique identifier
  name: string                     // Human-readable name
  description?: string             // What this resource contains
  type: MCPResourceType            // Resource category
  mimeType?: string               // Content type
  lastModified?: string           // ISO 8601 timestamp
  metadata?: Record<string, any>  // Additional properties
  content?: string                // Text content (for registration)
  blob?: Uint8Array              // Binary content (for registration)
}

Resource Types

type MCPResourceType =
  | 'file'       // File system files
  | 'directory'  // Folder structures
  | 'database'   // Database queries/results
  | 'api'        // External API endpoints
  | 'memory'     // In-memory data
  | 'web'        // Web URLs and content
  | 'custom'     // Plugin-defined types

URI Scheme

Resources use URI schemes to identify different data sources:

lokus://[plugin-id]/[resource-type]/[identifier]

Standard URI Patterns

// Notes
'lokus://notes/current'              // Currently open note
'lokus://notes/123'                  // Specific note by ID
'lokus://notes/all'                  // All notes
 
// Files
'file:///workspace/README.md'        // Absolute file path
'file://workspace/docs/*'            // File pattern
 
// Wiki
'lokus://wiki/HomePage'              // Wiki page
'lokus://wiki/tags/important'        // Pages by tag
 
// Plugin-specific
'myplugin://data/users'              // Plugin data
'myplugin://config/settings'         // Plugin configuration

Registering Resources

Server-Side Registration

Register resources in your MCP server plugin:

export default class MyPlugin {
  async activate(context) {
    const { mcp } = context
 
    // Simple text resource
    mcp.registerResource({
      uri: 'myplugin://hello',
      name: 'Hello Message',
      description: 'A simple greeting message',
      type: 'memory',
      mimeType: 'text/plain',
      content: 'Hello from my plugin!'
    })
 
    // JSON data resource
    mcp.registerResource({
      uri: 'myplugin://config',
      name: 'Plugin Configuration',
      description: 'Current plugin settings',
      type: 'memory',
      mimeType: 'application/json',
      content: JSON.stringify({
        enabled: true,
        theme: 'dark',
        options: { ... }
      })
    })
 
    // File resource
    mcp.registerResource({
      uri: 'file:///workspace/README.md',
      name: 'Project README',
      description: 'Project documentation',
      type: 'file',
      mimeType: 'text/markdown',
      content: await this.readFile('/workspace/README.md')
    })
  }
}

Using Resource Builder

Use the fluent builder API for complex resources:

import { MCPResourceBuilder } from '@lokus/mcp'
 
const resource = new MCPResourceBuilder()
  .setUri('myplugin://users/list')
  .setName('User List')
  .setDescription('All registered users')
  .setType('database')
  .setMimeType('application/json')
  .setMetadata({
    count: 150,
    lastUpdated: new Date().toISOString(),
    access: 'read-only'
  })
  .setContent(JSON.stringify(users))
  .build()
 
mcp.registerResource(resource)

Dynamic Resources

Register resources that compute content on-demand:

export default class DynamicResourcePlugin {
  async activate(context) {
    const { mcp } = context
 
    // Register resource handler
    mcp.registerResource({
      uri: 'myplugin://stats/current',
      name: 'Current Statistics',
      type: 'memory',
      mimeType: 'application/json',
      // Content will be computed when requested
      content: null,
      metadata: {
        dynamic: true
      }
    })
 
    // Handle resource read requests
    mcp.onResourceRead('myplugin://stats/current', async () => {
      const stats = await this.computeStats()
      return {
        contents: [{
          uri: 'myplugin://stats/current',
          mimeType: 'application/json',
          text: JSON.stringify(stats)
        }]
      }
    })
  }
 
  async computeStats() {
    return {
      notes: await this.countNotes(),
      files: await this.countFiles(),
      timestamp: new Date().toISOString()
    }
  }
}

Reading Resources

Client-Side Resource Access

import { MCPClient } from '@lokus/mcp'
 
const client = new MCPClient('my-client')
await client.connect(transport)
 
// List all resources
const response = await client.listResources()
console.log('Available resources:', response.resources)
 
// Read specific resource
const content = await client.readResource('lokus://notes/current')
console.log('Content:', content.contents[0].text)
 
// Read with error handling
try {
  const result = await client.readResource('myplugin://data')
  console.log('Success:', result)
} catch (error) {
  if (error.code === -32001) {
    console.error('Resource not found')
  } else if (error.code === -32002) {
    console.error('Access denied')
  } else {
    console.error('Error:', error.message)
  }
}

Pagination

Handle large resource lists with pagination:

async function listAllResources(client) {
  const allResources = []
  let cursor = null
 
  do {
    const response = await client.listResources({ cursor })
    allResources.push(...response.resources)
    cursor = response.nextCursor
  } while (cursor)
 
  return allResources
}
 
const resources = await listAllResources(client)
console.log(`Total resources: ${resources.length}`)

Filtering Resources

// Filter by type
const fileResources = response.resources.filter(r => r.type === 'file')
 
// Filter by URI pattern
const noteResources = response.resources.filter(r =>
  r.uri.startsWith('lokus://notes/')
)
 
// Filter by metadata
const writableResources = response.resources.filter(r =>
  r.metadata?.access === 'read-write'
)

Resource Subscriptions

Subscribe to resource changes for real-time updates:

Server-Side Updates

export default class SubscriptionPlugin {
  async activate(context) {
    const { mcp } = context
 
    // Register subscribable resource
    mcp.registerResource({
      uri: 'myplugin://counter',
      name: 'Counter Value',
      type: 'memory',
      mimeType: 'text/plain',
      content: '0'
    })
 
    // Update resource periodically
    this.interval = setInterval(() => {
      const newValue = (parseInt(this.currentValue) + 1).toString()
 
      // Update resource (triggers notifications)
      mcp.updateResource('myplugin://counter', newValue, {
        updated: new Date().toISOString()
      })
 
      this.currentValue = newValue
    }, 5000)
  }
 
  async deactivate() {
    clearInterval(this.interval)
  }
}

Client-Side Subscriptions

// Subscribe to resource
const subscription = await client.subscribeToResource('myplugin://counter')
 
// Handle updates
client.on('resource-updated', (event) => {
  if (event.uri === 'myplugin://counter') {
    console.log('Counter updated:', event.content)
    console.log('Metadata:', event.metadata)
  }
})
 
// Unsubscribe when done
await subscription.dispose()

Subscription Management

class ResourceManager {
  constructor(client) {
    this.client = client
    this.subscriptions = new Map()
  }
 
  async subscribe(uri, handler) {
    // Avoid duplicate subscriptions
    if (this.subscriptions.has(uri)) {
      return this.subscriptions.get(uri)
    }
 
    const subscription = await this.client.subscribeToResource(uri)
 
    const eventHandler = (event) => {
      if (event.uri === uri) {
        handler(event)
      }
    }
 
    this.client.on('resource-updated', eventHandler)
 
    this.subscriptions.set(uri, {
      subscription,
      handler: eventHandler
    })
 
    return subscription
  }
 
  async unsubscribe(uri) {
    const sub = this.subscriptions.get(uri)
    if (sub) {
      await sub.subscription.dispose()
      this.client.off('resource-updated', sub.handler)
      this.subscriptions.delete(uri)
    }
  }
 
  async unsubscribeAll() {
    for (const uri of this.subscriptions.keys()) {
      await this.unsubscribe(uri)
    }
  }
}

Resource Patterns

Pattern 1: Current Document

Expose the currently active document:

export default class EditorPlugin {
  async activate(context) {
    const { mcp, editor } = context
 
    // Register current document resource
    mcp.registerResource({
      uri: 'lokus://editor/current',
      name: 'Current Document',
      description: 'Currently active document',
      type: 'file',
      mimeType: 'text/markdown'
    })
 
    // Update when document changes
    editor.onDidChangeActiveDocument((doc) => {
      mcp.updateResource('lokus://editor/current', doc.content, {
        path: doc.path,
        language: doc.language,
        modified: doc.isDirty
      })
    })
 
    // Update when content changes
    editor.onDidChangeTextDocument((event) => {
      if (event.document.uri === editor.activeDocument?.uri) {
        mcp.updateResource('lokus://editor/current', event.document.content, {
          changeCount: this.changeCount++
        })
      }
    })
  }
}

Pattern 2: Search Results

Expose search results as a resource:

export default class SearchPlugin {
  async activate(context) {
    const { mcp } = context
 
    // Register search resource
    mcp.registerResource({
      uri: 'lokus://search/results',
      name: 'Search Results',
      type: 'memory',
      mimeType: 'application/json',
      content: JSON.stringify([])
    })
 
    // Handle dynamic read
    mcp.onResourceRead('lokus://search/results', async () => {
      const query = this.currentQuery
      const results = await this.search(query)
 
      return {
        contents: [{
          uri: 'lokus://search/results',
          mimeType: 'application/json',
          text: JSON.stringify({
            query,
            count: results.length,
            results
          })
        }]
      }
    })
  }
 
  async performSearch(query) {
    this.currentQuery = query
    const results = await this.search(query)
 
    // Notify subscribers
    mcp.updateResource('lokus://search/results', JSON.stringify(results), {
      query,
      count: results.length,
      timestamp: new Date().toISOString()
    })
 
    return results
  }
}

Pattern 3: Database Query

Expose database data as resources:

export default class DatabasePlugin {
  async activate(context) {
    const { mcp } = context
 
    // Register database resources
    const tables = await this.getTables()
 
    for (const table of tables) {
      mcp.registerResource({
        uri: `db://${table.name}`,
        name: `${table.name} Table`,
        description: `Access to ${table.name} database table`,
        type: 'database',
        mimeType: 'application/json',
        metadata: {
          rowCount: table.rowCount,
          columns: table.columns
        }
      })
 
      // Handle queries
      mcp.onResourceRead(`db://${table.name}`, async (params) => {
        const limit = params.limit || 100
        const offset = params.offset || 0
 
        const rows = await this.query(
          `SELECT * FROM ${table.name} LIMIT ${limit} OFFSET ${offset}`
        )
 
        return {
          contents: [{
            uri: `db://${table.name}`,
            mimeType: 'application/json',
            text: JSON.stringify(rows)
          }]
        }
      })
    }
  }
}

Pattern 4: File System

Expose file system as navigable resources:

export default class FileSystemPlugin {
  async activate(context) {
    const { mcp } = context
 
    // Register root directory
    await this.registerDirectory('file://workspace', '/workspace')
  }
 
  async registerDirectory(uri, path) {
    const stats = await this.getDirectoryStats(path)
 
    // Register directory resource
    mcp.registerResource({
      uri,
      name: basename(path),
      description: `Directory: ${path}`,
      type: 'directory',
      mimeType: 'application/json',
      metadata: {
        path,
        fileCount: stats.files,
        directoryCount: stats.directories
      }
    })
 
    // Handle directory listing
    mcp.onResourceRead(uri, async () => {
      const entries = await this.readDirectory(path)
 
      return {
        contents: [{
          uri,
          mimeType: 'application/json',
          text: JSON.stringify({
            path,
            entries: entries.map(e => ({
              name: e.name,
              type: e.type,
              uri: `${uri}/${e.name}`,
              size: e.size,
              modified: e.modified
            }))
          })
        }]
      }
    })
 
    // Register child resources
    const entries = await this.readDirectory(path)
    for (const entry of entries) {
      if (entry.type === 'file') {
        await this.registerFile(`${uri}/${entry.name}`, `${path}/${entry.name}`)
      } else if (entry.type === 'directory') {
        await this.registerDirectory(`${uri}/${entry.name}`, `${path}/${entry.name}`)
      }
    }
  }
 
  async registerFile(uri, path) {
    const stats = await this.getFileStats(path)
 
    mcp.registerResource({
      uri,
      name: basename(path),
      description: `File: ${path}`,
      type: 'file',
      mimeType: this.getMimeType(path),
      metadata: {
        path,
        size: stats.size,
        modified: stats.modified
      }
    })
 
    // Handle file read
    mcp.onResourceRead(uri, async () => {
      const content = await this.readFile(path)
 
      return {
        contents: [{
          uri,
          mimeType: this.getMimeType(path),
          text: content
        }]
      }
    })
  }
}

Advanced Topics

Resource Caching

Implement client-side caching:

class CachedResourceClient {
  constructor(client, options = {}) {
    this.client = client
    this.cache = new Map()
    this.cacheTTL = options.cacheTTL || 60000  // 1 minute
  }
 
  async readResource(uri, options = {}) {
    const { skipCache = false } = options
 
    // Check cache
    if (!skipCache) {
      const cached = this.cache.get(uri)
      if (cached && Date.now() - cached.timestamp < this.cacheTTL) {
        return cached.content
      }
    }
 
    // Fetch from server
    const content = await this.client.readResource(uri)
 
    // Update cache
    this.cache.set(uri, {
      content,
      timestamp: Date.now()
    })
 
    return content
  }
 
  invalidateCache(uri) {
    if (uri) {
      this.cache.delete(uri)
    } else {
      this.cache.clear()
    }
  }
 
  // Subscribe and auto-invalidate
  async subscribeToResource(uri) {
    const subscription = await this.client.subscribeToResource(uri)
 
    this.client.on('resource-updated', (event) => {
      if (event.uri === uri) {
        this.invalidateCache(uri)
      }
    })
 
    return subscription
  }
}

Access Control

Implement resource-level permissions:

export default class SecureResourcePlugin {
  async activate(context) {
    const { mcp, security } = context
 
    // Register protected resource
    mcp.registerResource({
      uri: 'secure://admin/settings',
      name: 'Admin Settings',
      type: 'memory',
      mimeType: 'application/json',
      metadata: {
        requiredPermission: 'admin:read'
      }
    })
 
    // Intercept resource reads
    mcp.onResourceRead('secure://admin/settings', async (params, clientInfo) => {
      // Check permissions
      if (!await security.hasPermission(clientInfo.id, 'admin:read')) {
        throw {
          code: -32002,
          message: 'Access denied: insufficient permissions',
          data: { requiredPermission: 'admin:read' }
        }
      }
 
      // Return protected content
      return {
        contents: [{
          uri: 'secure://admin/settings',
          mimeType: 'application/json',
          text: JSON.stringify(this.adminSettings)
        }]
      }
    })
  }
}

Resource Versioning

Track resource versions:

export default class VersionedResourcePlugin {
  constructor() {
    this.versions = new Map()
  }
 
  async activate(context) {
    const { mcp } = context
 
    // Register versioned resource
    mcp.registerResource({
      uri: 'versioned://document',
      name: 'Versioned Document',
      type: 'file',
      mimeType: 'text/markdown',
      metadata: {
        version: 1,
        previousVersions: []
      }
    })
  }
 
  async updateDocument(content) {
    const currentVersion = this.versions.get('document') || { version: 0, content: '' }
    const newVersion = currentVersion.version + 1
 
    // Save previous version
    this.versions.set(`document@${currentVersion.version}`, currentVersion)
 
    // Update current version
    const versionedContent = {
      version: newVersion,
      content,
      timestamp: new Date().toISOString(),
      previousVersion: currentVersion.version
    }
 
    this.versions.set('document', versionedContent)
 
    // Update resource
    mcp.updateResource('versioned://document', content, {
      version: newVersion,
      previousVersions: Array.from({ length: newVersion - 1 }, (_, i) => i + 1)
    })
 
    return newVersion
  }
 
  async getVersion(version) {
    return this.versions.get(`document@${version}`)
  }
}

Best Practices

1. URI Design

  • Use consistent naming: plugin://category/identifier
  • Keep URIs stable across versions
  • Use meaningful identifiers, not internal IDs
  • Support hierarchical structures when appropriate

2. Resource Metadata

  • Include useful metadata (size, timestamps, counts)
  • Document metadata fields in resource description
  • Use standardized metadata keys when possible
  • Keep metadata small and relevant

3. Content Format

  • Use appropriate MIME types
  • Prefer JSON for structured data
  • Include character encoding for text
  • Consider compression for large content

4. Performance

  • Register resources lazily when possible
  • Implement pagination for large lists
  • Cache frequently accessed resources
  • Use subscriptions instead of polling

5. Error Handling

  • Return appropriate error codes
  • Provide helpful error messages
  • Include recovery suggestions in error data
  • Log errors for debugging

Next Steps

Related Documentation: