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
notnc
- 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: