MCP Tools

Tools in MCP enable AI assistants to perform actions and operations within Lokus. They provide a standardized interface for executing functions, commands, and workflows.

Overview

An MCP tool is a callable function that:

  • Executes actions: Performs operations in Lokus
  • Validates inputs: Uses JSON Schema for parameter validation
  • Returns results: Provides structured output
  • Handles errors: Reports failures gracefully

Tool Structure

Basic Tool Definition

interface MCPTool {
  name: string                    // Unique tool identifier
  description: string             // What the tool does
  inputSchema: JSONSchema         // Parameter validation schema
  type?: MCPToolType             // Tool category
  execute?: (args: any) => any   // Implementation function
}

Tool Types

type MCPToolType =
  | 'function'   // Direct function execution
  | 'command'    // Application commands
  | 'api_call'   // External API operations
  | 'script'     // Script execution
  | 'query'      // Database/search queries

Registering Tools

Basic Tool Registration

export default class MyPlugin {
  async activate(context) {
    const { mcp } = context
 
    // Simple tool
    mcp.registerTool({
      name: 'greet',
      description: 'Generate a greeting message',
      inputSchema: {
        type: 'object',
        properties: {
          name: {
            type: 'string',
            description: 'Name to greet'
          }
        },
        required: ['name']
      },
      execute: async ({ name }) => {
        return {
          output: `Hello, ${name}!`
        }
      }
    })
  }
}

Using Tool Builder

import { MCPToolBuilder } from '@lokus/mcp'
 
const tool = new MCPToolBuilder()
  .setName('createNote')
  .setDescription('Create a new note in the workspace')
  .setInputSchema({
    type: 'object',
    properties: {
      title: {
        type: 'string',
        description: 'Note title'
      },
      content: {
        type: 'string',
        description: 'Note content'
      },
      tags: {
        type: 'array',
        items: { type: 'string' },
        description: 'Note tags'
      }
    },
    required: ['title', 'content']
  })
  .setExecutor(async ({ title, content, tags = [] }) => {
    const note = await this.createNote(title, content, tags)
    return {
      output: `Created note: ${note.id}`,
      noteId: note.id,
      path: note.path
    }
  })
  .build()
 
mcp.registerTool(tool)

Complete Tool Example

export default class NoteToolsPlugin {
  async activate(context) {
    const { mcp, workspace, editor } = context
 
    // Create note tool
    mcp.registerTool({
      name: 'note.create',
      description: 'Create a new note with title and content',
      type: 'function',
      inputSchema: {
        type: 'object',
        properties: {
          title: {
            type: 'string',
            description: 'Note title',
            minLength: 1,
            maxLength: 200
          },
          content: {
            type: 'string',
            description: 'Note content in Markdown format',
            default: ''
          },
          folder: {
            type: 'string',
            description: 'Folder path (relative to workspace)',
            default: '/'
          },
          tags: {
            type: 'array',
            items: { type: 'string' },
            description: 'Tags for the note',
            default: []
          },
          template: {
            type: 'string',
            description: 'Template to use',
            enum: ['blank', 'meeting', 'task', 'journal']
          }
        },
        required: ['title']
      },
      execute: async (args) => {
        try {
          // Validate folder
          const folderPath = workspace.resolvePath(args.folder)
          await workspace.ensureFolder(folderPath)
 
          // Apply template
          let content = args.content
          if (args.template) {
            content = await this.applyTemplate(args.template, args)
          }
 
          // Create note
          const note = await workspace.createNote({
            title: args.title,
            content,
            folder: folderPath,
            tags: args.tags
          })
 
          // Open in editor
          await editor.openNote(note.id)
 
          return {
            output: `Created note "${args.title}" at ${note.path}`,
            noteId: note.id,
            path: note.path,
            uri: note.uri
          }
        } catch (error) {
          throw {
            code: -32012,
            message: `Failed to create note: ${error.message}`,
            data: { error: error.toString() }
          }
        }
      }
    })
 
    // Search notes tool
    mcp.registerTool({
      name: 'note.search',
      description: 'Search notes by content, title, or tags',
      inputSchema: {
        type: 'object',
        properties: {
          query: {
            type: 'string',
            description: 'Search query'
          },
          tags: {
            type: 'array',
            items: { type: 'string' },
            description: 'Filter by tags'
          },
          limit: {
            type: 'number',
            description: 'Maximum results',
            default: 10,
            minimum: 1,
            maximum: 100
          }
        },
        required: ['query']
      },
      execute: async ({ query, tags, limit = 10 }) => {
        const results = await workspace.searchNotes({
          query,
          tags,
          limit
        })
 
        return {
          output: `Found ${results.length} notes`,
          count: results.length,
          results: results.map(note => ({
            id: note.id,
            title: note.title,
            path: note.path,
            excerpt: note.excerpt,
            tags: note.tags
          }))
        }
      }
    })
 
    // Batch update tool
    mcp.registerTool({
      name: 'note.batchUpdate',
      description: 'Update multiple notes at once',
      inputSchema: {
        type: 'object',
        properties: {
          noteIds: {
            type: 'array',
            items: { type: 'string' },
            description: 'Note IDs to update'
          },
          operation: {
            type: 'string',
            enum: ['addTag', 'removeTag', 'move', 'delete'],
            description: 'Operation to perform'
          },
          operationData: {
            type: 'object',
            description: 'Operation-specific data'
          }
        },
        required: ['noteIds', 'operation']
      },
      execute: async ({ noteIds, operation, operationData }) => {
        const results = []
 
        for (const noteId of noteIds) {
          try {
            switch (operation) {
              case 'addTag':
                await workspace.addNoteTag(noteId, operationData.tag)
                results.push({ noteId, success: true })
                break
              case 'removeTag':
                await workspace.removeNoteTag(noteId, operationData.tag)
                results.push({ noteId, success: true })
                break
              case 'move':
                await workspace.moveNote(noteId, operationData.destination)
                results.push({ noteId, success: true })
                break
              case 'delete':
                await workspace.deleteNote(noteId)
                results.push({ noteId, success: true })
                break
            }
          } catch (error) {
            results.push({
              noteId,
              success: false,
              error: error.message
            })
          }
        }
 
        const successCount = results.filter(r => r.success).length
 
        return {
          output: `Updated ${successCount}/${noteIds.length} notes`,
          results
        }
      }
    })
  }
}

Calling Tools

Client-Side Tool Execution

import { MCPClient } from '@lokus/mcp'
 
const client = new MCPClient('my-client')
await client.connect(transport)
 
// List available tools
const tools = await client.listTools()
console.log('Available tools:', tools.tools.map(t => t.name))
 
// Call a tool
const result = await client.callTool('note.create', {
  title: 'Meeting Notes',
  content: '# Team Meeting\n\n## Agenda\n- Project updates',
  tags: ['meeting', 'team']
})
 
console.log(result.content[0].text)  // Tool output
 
// Handle errors
try {
  await client.callTool('note.create', {
    // Missing required 'title' parameter
    content: 'Test'
  })
} catch (error) {
  if (error.code === -32013) {
    console.error('Invalid input:', error.message)
  }
}

Tool Discovery

// Find tools by name pattern
const noteTools = tools.tools.filter(t => t.name.startsWith('note.'))
 
// Find tools by description keyword
const searchTools = tools.tools.filter(t =>
  t.description.toLowerCase().includes('search')
)
 
// Inspect tool schema
const createNoteTool = tools.tools.find(t => t.name === 'note.create')
console.log('Required parameters:', createNoteTool.inputSchema.required)
console.log('Properties:', createNoteTool.inputSchema.properties)

Tool Patterns

Pattern 1: CRUD Operations

export default class CRUDPlugin {
  async activate(context) {
    const { mcp } = context
 
    // Create
    mcp.registerTool({
      name: 'item.create',
      description: 'Create a new item',
      inputSchema: {
        type: 'object',
        properties: {
          name: { type: 'string' },
          data: { type: 'object' }
        },
        required: ['name']
      },
      execute: async ({ name, data = {} }) => {
        const item = await this.db.create({ name, data })
        return {
          output: `Created item: ${item.id}`,
          itemId: item.id
        }
      }
    })
 
    // Read
    mcp.registerTool({
      name: 'item.get',
      description: 'Get item by ID',
      inputSchema: {
        type: 'object',
        properties: {
          id: { type: 'string' }
        },
        required: ['id']
      },
      execute: async ({ id }) => {
        const item = await this.db.findById(id)
        if (!item) {
          throw { code: -32001, message: 'Item not found' }
        }
        return {
          output: JSON.stringify(item, null, 2),
          item
        }
      }
    })
 
    // Update
    mcp.registerTool({
      name: 'item.update',
      description: 'Update an item',
      inputSchema: {
        type: 'object',
        properties: {
          id: { type: 'string' },
          data: { type: 'object' }
        },
        required: ['id', 'data']
      },
      execute: async ({ id, data }) => {
        const item = await this.db.update(id, data)
        return {
          output: `Updated item: ${id}`,
          item
        }
      }
    })
 
    // Delete
    mcp.registerTool({
      name: 'item.delete',
      description: 'Delete an item',
      inputSchema: {
        type: 'object',
        properties: {
          id: { type: 'string' }
        },
        required: ['id']
      },
      execute: async ({ id }) => {
        await this.db.delete(id)
        return {
          output: `Deleted item: ${id}`
        }
      }
    })
  }
}

Pattern 2: Workflow Automation

export default class WorkflowPlugin {
  async activate(context) {
    const { mcp } = context
 
    mcp.registerTool({
      name: 'workflow.createProjectStructure',
      description: 'Create a complete project structure with folders and files',
      inputSchema: {
        type: 'object',
        properties: {
          projectName: { type: 'string' },
          type: {
            type: 'string',
            enum: ['web', 'mobile', 'library', 'documentation']
          },
          features: {
            type: 'array',
            items: { type: 'string' }
          }
        },
        required: ['projectName', 'type']
      },
      execute: async ({ projectName, type, features = [] }) => {
        const structure = this.getProjectStructure(type)
        const created = []
 
        // Create folders
        for (const folder of structure.folders) {
          await workspace.createFolder(`${projectName}/${folder}`)
          created.push(`📁 ${folder}`)
        }
 
        // Create files from templates
        for (const file of structure.files) {
          const content = await this.getTemplate(file.template, {
            projectName,
            type,
            features
          })
          await workspace.createFile(`${projectName}/${file.path}`, content)
          created.push(`📄 ${file.path}`)
        }
 
        // Initialize features
        for (const feature of features) {
          await this.initializeFeature(projectName, feature)
          created.push(`✨ ${feature}`)
        }
 
        return {
          output: `Created project structure for "${projectName}"\n\n${created.join('\n')}`,
          projectPath: projectName,
          filesCreated: created.length
        }
      }
    })
  }
}

Pattern 3: External API Integration

export default class APIPlugin {
  async activate(context) {
    const { mcp } = context
 
    mcp.registerTool({
      name: 'github.createIssue',
      description: 'Create a GitHub issue',
      type: 'api_call',
      inputSchema: {
        type: 'object',
        properties: {
          title: { type: 'string' },
          body: { type: 'string' },
          labels: {
            type: 'array',
            items: { type: 'string' }
          },
          assignees: {
            type: 'array',
            items: { type: 'string' }
          }
        },
        required: ['title']
      },
      execute: async ({ title, body = '', labels = [], assignees = [] }) => {
        const config = await this.getGitHubConfig()
 
        const response = await fetch(
          `https://api.github.com/repos/${config.owner}/${config.repo}/issues`,
          {
            method: 'POST',
            headers: {
              'Authorization': `token ${config.token}`,
              'Content-Type': 'application/json'
            },
            body: JSON.stringify({
              title,
              body,
              labels,
              assignees
            })
          }
        )
 
        if (!response.ok) {
          throw {
            code: -32012,
            message: 'GitHub API error',
            data: await response.text()
          }
        }
 
        const issue = await response.json()
 
        return {
          output: `Created issue #${issue.number}: ${issue.title}`,
          issueNumber: issue.number,
          url: issue.html_url
        }
      }
    })
  }
}

Advanced Features

Async Tool Execution

For long-running operations:

mcp.registerTool({
  name: 'export.large',
  description: 'Export large dataset (may take several minutes)',
  inputSchema: {
    type: 'object',
    properties: {
      format: {
        type: 'string',
        enum: ['json', 'csv', 'xml']
      },
      filters: { type: 'object' }
    },
    required: ['format']
  },
  execute: async ({ format, filters }) => {
    // Return immediately with job ID
    const jobId = generateId()
 
    // Process asynchronously
    this.processExportJob(jobId, format, filters)
 
    return {
      output: `Export started. Job ID: ${jobId}`,
      jobId,
      status: 'processing',
      checkStatusTool: 'export.status'
    }
  }
})
 
mcp.registerTool({
  name: 'export.status',
  description: 'Check export job status',
  inputSchema: {
    type: 'object',
    properties: {
      jobId: { type: 'string' }
    },
    required: ['jobId']
  },
  execute: async ({ jobId }) => {
    const job = await this.getJob(jobId)
 
    return {
      output: `Job ${jobId}: ${job.status} (${job.progress}%)`,
      status: job.status,
      progress: job.progress,
      result: job.result
    }
  }
})

Progress Reporting

mcp.registerTool({
  name: 'batch.process',
  description: 'Process multiple items with progress updates',
  inputSchema: {
    type: 'object',
    properties: {
      items: {
        type: 'array',
        items: { type: 'string' }
      }
    },
    required: ['items']
  },
  execute: async ({ items }, context) => {
    const results = []
    const total = items.length
 
    for (let i = 0; i < items.length; i++) {
      const item = items[i]
 
      // Process item
      const result = await this.processItem(item)
      results.push(result)
 
      // Report progress (if context supports it)
      if (context.reportProgress) {
        context.reportProgress({
          current: i + 1,
          total,
          message: `Processing ${item}`
        })
      }
    }
 
    return {
      output: `Processed ${total} items`,
      results
    }
  }
})

Tool Composition

Chain multiple tools together:

mcp.registerTool({
  name: 'workflow.createAndPopulateNote',
  description: 'Create a note and populate it with search results',
  inputSchema: {
    type: 'object',
    properties: {
      title: { type: 'string' },
      searchQuery: { type: 'string' }
    },
    required: ['title', 'searchQuery']
  },
  execute: async ({ title, searchQuery }, context) => {
    // Call note.create tool
    const createResult = await context.callTool('note.create', {
      title,
      content: '# Loading...'
    })
 
    // Call note.search tool
    const searchResult = await context.callTool('note.search', {
      query: searchQuery,
      limit: 10
    })
 
    // Format search results
    const content = this.formatSearchResults(searchResult.results)
 
    // Call note.update tool
    await context.callTool('note.update', {
      id: createResult.noteId,
      content
    })
 
    return {
      output: `Created note "${title}" with ${searchResult.count} search results`,
      noteId: createResult.noteId,
      searchCount: searchResult.count
    }
  }
})

Best Practices

1. Tool Naming

  • Use dot notation: category.action
  • Be descriptive: note.create not nc
  • Follow conventions: Use standard CRUD names
  • Avoid conflicts: Include plugin name if needed

2. Input Validation

  • Use comprehensive JSON Schema
  • Provide default values
  • Include min/max constraints
  • Add helpful descriptions
  • Validate enums for options

3. Error Handling

  • Use standard MCP error codes
  • Provide actionable error messages
  • Include error context in data field
  • Log errors for debugging
  • Handle edge cases gracefully

4. Return Values

  • Always include output string
  • Return relevant data fields
  • Use consistent structure
  • Document return format
  • Handle different result types

5. Performance

  • Keep tools focused and fast
  • Use async for I/O operations
  • Implement timeouts
  • Report progress for long operations
  • Cache when appropriate

Next Steps

Related Documentation: