MCP Plugin Integration
Learn how to create plugins that integrate with Lokus’s MCP system to expose resources, tools, and prompts to AI assistants.
Plugin Types
MCP Server Plugin
Exposes capabilities to AI assistants:
{
"id": "my-mcp-server",
"name": "My MCP Server",
"version": "1.0.0",
"main": "index.js",
"type": "mcp-server",
"permissions": [
"mcp:serve",
"mcp:resources:read",
"mcp:resources:write",
"mcp:tools:execute"
],
"mcp": {
"type": "mcp-server",
"capabilities": {
"resources": {
"subscribe": true,
"listChanged": true
},
"tools": {
"listChanged": true
}
}
}
}
export default class MyMCPServer {
async activate(context) {
const { mcp } = context
// Register resources
mcp.registerResource({
uri: 'myplugin://data',
name: 'Plugin Data',
type: 'memory',
content: 'data'
})
// Register tools
mcp.registerTool({
name: 'myTool',
description: 'My tool',
inputSchema: { type: 'object' },
execute: async (args) => ({ output: 'result' })
})
// Register prompts
mcp.registerPrompt({
name: 'myPrompt',
description: 'My prompt',
template: 'Template text with {{variable}}'
})
}
}
MCP Client Plugin
Consumes MCP services:
{
"id": "my-mcp-client",
"name": "My MCP Client",
"version": "1.0.0",
"main": "index.js",
"type": "mcp-client",
"permissions": [
"mcp:client",
"mcp:resources:read",
"mcp:tools:execute"
]
}
export default class MyMCPClient {
async activate(context) {
const { mcp } = context
// Create client
this.client = mcp.createClient()
// Connect to server
await this.client.connect(transport)
// Use resources
const resources = await this.client.listResources()
// Call tools
const result = await this.client.callTool('someTool', { arg: 'value' })
}
async deactivate() {
await this.client.disconnect()
}
}
Hybrid Plugin
Acts as both server and client:
{
"id": "my-hybrid-plugin",
"name": "My Hybrid Plugin",
"version": "1.0.0",
"main": "index.js",
"type": "mcp-hybrid",
"permissions": [
"mcp:serve",
"mcp:client",
"mcp:resources:read",
"mcp:resources:write",
"mcp:tools:execute"
]
}
export default class MyHybridPlugin {
async activate(context) {
const { mcp } = context
// Expose own capabilities
mcp.registerTool({
name: 'exportData',
description: 'Export data',
inputSchema: { type: 'object' },
execute: async (args) => this.exportData(args)
})
// Consume other plugins' capabilities
const tools = mcp.findTools({ category: 'import' })
for (const tool of tools) {
console.log('Available tool:', tool.name)
}
}
}
Complete Example
Here’s a full-featured MCP plugin:
// manifest.json
{
"id": "lokus-note-tools",
"name": "Lokus Note Tools",
"version": "1.0.0",
"description": "MCP tools for note management",
"main": "index.js",
"type": "mcp-server",
"author": "Your Name",
"license": "MIT",
"permissions": [
"mcp:serve",
"mcp:resources:read",
"mcp:resources:write",
"mcp:tools:execute",
"workspace:read",
"workspace:write",
"editor:read",
"editor:write"
],
"mcp": {
"type": "mcp-server",
"enableResourceSubscriptions": true,
"enableToolExecution": true,
"enablePromptTemplates": true,
"memoryLimit": 134217728,
"cpuTimeLimit": 1000
}
}
// index.js
import { MCPResourceBuilder, MCPToolBuilder, MCPPromptBuilder } from '@lokus/mcp'
export default class NoteToolsPlugin {
constructor() {
this.subscriptions = []
}
async activate(context) {
const { mcp, workspace, editor, events } = context
// Register resources
await this.registerResources(mcp, workspace, editor)
// Register tools
await this.registerTools(mcp, workspace, editor)
// Register prompts
await this.registerPrompts(mcp)
// Set up event handlers
this.setupEventHandlers(mcp, workspace, editor, events)
console.log('Note Tools MCP Plugin activated')
}
async registerResources(mcp, workspace, editor) {
// Current note resource
const currentNote = new MCPResourceBuilder()
.setUri('lokus://notes/current')
.setName('Current Note')
.setDescription('Currently active note in the editor')
.setType('file')
.setMimeType('text/markdown')
.setMetadata({ dynamic: true })
.build()
mcp.registerResource(currentNote)
// All notes resource
const allNotes = new MCPResourceBuilder()
.setUri('lokus://notes/all')
.setName('All Notes')
.setDescription('List of all notes in the workspace')
.setType('database')
.setMimeType('application/json')
.setMetadata({ dynamic: true })
.build()
mcp.registerResource(allNotes)
// Handle dynamic resource reads
mcp.onResourceRead('lokus://notes/current', async () => {
const note = await editor.getCurrentNote()
return {
contents: [{
uri: 'lokus://notes/current',
mimeType: 'text/markdown',
text: note ? note.content : ''
}]
}
})
mcp.onResourceRead('lokus://notes/all', async () => {
const notes = await workspace.getAllNotes()
return {
contents: [{
uri: 'lokus://notes/all',
mimeType: 'application/json',
text: JSON.stringify(notes, null, 2)
}]
}
})
}
async registerTools(mcp, workspace, editor) {
// Create note tool
const createNote = new MCPToolBuilder()
.setName('note.create')
.setDescription('Create a new note')
.setInputSchema({
type: 'object',
properties: {
title: { type: 'string', description: 'Note title' },
content: { type: 'string', description: 'Note content', default: '' },
tags: { type: 'array', items: { type: 'string' }, default: [] }
},
required: ['title']
})
.setExecutor(async ({ title, content, tags }) => {
const note = await workspace.createNote({ title, content, tags })
await editor.openNote(note.id)
return {
output: `Created note: ${title}`,
noteId: note.id,
path: note.path
}
})
.build()
mcp.registerTool(createNote)
// Search notes tool
const searchNotes = new MCPToolBuilder()
.setName('note.search')
.setDescription('Search notes by content or title')
.setInputSchema({
type: 'object',
properties: {
query: { type: 'string', description: 'Search query' },
limit: { type: 'number', default: 10, minimum: 1, maximum: 100 }
},
required: ['query']
})
.setExecutor(async ({ query, limit }) => {
const results = await workspace.searchNotes({ query, limit })
return {
output: `Found ${results.length} notes`,
count: results.length,
results
}
})
.build()
mcp.registerTool(searchNotes)
// Update note tool
const updateNote = new MCPToolBuilder()
.setName('note.update')
.setDescription('Update an existing note')
.setInputSchema({
type: 'object',
properties: {
noteId: { type: 'string', description: 'Note ID' },
content: { type: 'string', description: 'New content' }
},
required: ['noteId', 'content']
})
.setExecutor(async ({ noteId, content }) => {
await workspace.updateNote(noteId, { content })
return {
output: `Updated note: ${noteId}`,
noteId
}
})
.build()
mcp.registerTool(updateNote)
}
async registerPrompts(mcp) {
// Summarize note prompt
const summarize = new MCPPromptBuilder()
.setName('note.summarize')
.setDescription('Generate a summary of a note')
.setTemplate(`Summarize the following note in 2-3 sentences:
Title: {{title}}
Content:
{{content}}`)
.setArguments([
{ name: 'title', required: true, type: 'string' },
{ name: 'content', required: true, type: 'string' }
])
.build()
mcp.registerPrompt(summarize)
// Improve note prompt
const improve = new MCPPromptBuilder()
.setName('note.improve')
.setDescription('Suggest improvements for a note')
.setTemplate(`Review and suggest improvements for this note:
Title: {{title}}
Content:
{{content}}
Provide specific suggestions for:
1. Content organization
2. Clarity and readability
3. Missing information
4. Overall structure`)
.setArguments([
{ name: 'title', required: true, type: 'string' },
{ name: 'content', required: true, type: 'string' }
])
.build()
mcp.registerPrompt(improve)
}
setupEventHandlers(mcp, workspace, editor, events) {
// Update current note resource when active note changes
this.subscriptions.push(
editor.onDidChangeActiveDocument((doc) => {
if (doc) {
mcp.updateResource('lokus://notes/current', doc.content, {
noteId: doc.id,
path: doc.path,
modified: doc.isDirty
})
}
})
)
// Update notes list when workspace changes
this.subscriptions.push(
workspace.onDidChangeFiles((event) => {
if (event.changes.some(c => c.type === 'note')) {
mcp.sendNotification('notifications/resources/list_changed')
}
})
)
}
async deactivate() {
// Clean up subscriptions
this.subscriptions.forEach(sub => sub.dispose())
this.subscriptions = []
console.log('Note Tools MCP Plugin deactivated')
}
}
Testing Your Plugin
// test/plugin.test.js
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { createMockMCPServer, createMockMCPClient } from '@lokus/mcp/testing'
import NoteToolsPlugin from '../index.js'
describe('Note Tools MCP Plugin', () => {
let plugin
let mockContext
let mcpServer
let mcpClient
beforeEach(async () => {
// Create mock MCP server
mcpServer = createMockMCPServer()
// Create mock context
mockContext = {
mcp: mcpServer,
workspace: createMockWorkspace(),
editor: createMockEditor(),
events: createMockEvents()
}
// Create and activate plugin
plugin = new NoteToolsPlugin()
await plugin.activate(mockContext)
// Create client to test
mcpClient = createMockMCPClient()
await mcpClient.connect(mcpServer.getTransport())
})
afterEach(async () => {
await plugin.deactivate()
await mcpClient.disconnect()
await mcpServer.stop()
})
it('should register resources', async () => {
const resources = await mcpClient.listResources()
expect(resources.resources).toContainEqual(
expect.objectContaining({ uri: 'lokus://notes/current' })
)
expect(resources.resources).toContainEqual(
expect.objectContaining({ uri: 'lokus://notes/all' })
)
})
it('should register tools', async () => {
const tools = await mcpClient.listTools()
const toolNames = tools.tools.map(t => t.name)
expect(toolNames).toContain('note.create')
expect(toolNames).toContain('note.search')
expect(toolNames).toContain('note.update')
})
it('should create a note', async () => {
const result = await mcpClient.callTool('note.create', {
title: 'Test Note',
content: 'Test content',
tags: ['test']
})
expect(result.isError).toBe(false)
expect(result.content[0].text).toContain('Created note')
})
it('should search notes', async () => {
const result = await mcpClient.callTool('note.search', {
query: 'test',
limit: 5
})
expect(result.isError).toBe(false)
expect(result.content[0].text).toContain('Found')
})
})
Publishing
# Build plugin
npm run build
# Validate
lokus-plugin validate
# Package
lokus-plugin package
# Publish
lokus-plugin publish
Next Steps
Related Documentation: