Creating Custom Skills
This guide shows you how to develop new skills for OpenWork using the Model Context Protocol (MCP) standard. Skills extend OpenWork’s capabilities with specialized automation tools while maintaining security and modularity.
Overview
A skill is a standalone Node.js package that:
- Exposes tools through the MCP protocol
- Communicates with the main app via HTTP or IPC
- Runs in a separate process for isolation
- Follows standard npm conventions
Quick Start
1. Create Skill Directory
mkdir ~/openwork-docs/skills/my-skill
cd ~/openwork-docs/skills/my-skill
2. Initialize Package
{
"name": "my-skill",
"version": "1.0.0",
"type": "module",
"description": "My custom OpenWork skill",
"scripts": {
"start": "npx tsx src/index.ts",
"dev": "npx tsx --watch src/index.ts"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"tsx": "^4.21.0",
"typescript": "^5.0.0"
}
}
3. Basic Implementation
// src/index.ts
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
type CallToolResult,
} from '@modelcontextprotocol/sdk/types.js';
const server = new Server(
{ name: 'my-skill', version: '1.0.0' },
{ capabilities: { tools: {} } }
);
// Define your tools
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'my_tool',
description: 'Description of what this tool does',
inputSchema: {
type: 'object',
properties: {
param1: { type: 'string', description: 'Parameter 1' },
param2: { type: 'number', description: 'Parameter 2' }
},
required: ['param1']
}
}
]
}));
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request): Promise<CallToolResult> => {
const args = request.params.arguments;
switch (request.params.name) {
case 'my_tool':
return {
content: [{ type: 'text', text: `Processed: ${JSON.stringify(args)}` }]
};
default:
return {
content: [{ type: 'text', text: `Unknown tool: ${request.params.name}` }],
isError: true
};
}
});
// Start server
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('My Skill MCP Server started');
4. TypeScript Configuration
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
Advanced Patterns
HTTP API Integration
For skills that need to integrate with the Electron UI:
// HTTP server for UI integration
import { createServer } from 'http';
const HTTP_PORT = process.env.HTTP_PORT || '8080';
const server = createServer(async (req, res) => {
if (req.method === 'POST' && req.url === '/ui-endpoint') {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
const data = JSON.parse(body);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'success' }));
});
}
});
server.listen(HTTP_PORT, () => {
console.error(`HTTP server listening on port ${HTTP_PORT}`);
});
File Permission Integration
For skills that need file access:
// Integrate with file-permission skill
async function safeFileOperation(operation: string, filePath: string) {
// Request permission first
const permissionResponse = await fetch('http://localhost:9226/permission', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ operation, filePath })
});
const result = await permissionResponse.json();
if (!result.allowed) {
throw new Error('Permission denied');
}
// Proceed with file operation
// ... your file logic here
}
WebSocket Communication
For real-time data exchange:
// WebSocket server for real-time updates
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8081 });
wss.on('connection', (ws) => {
ws.on('message', (message) => {
const data = JSON.parse(message.toString());
console.log('Received:', data);
// Send response
ws.send(JSON.stringify({
type: 'response',
data: { status: 'processed' }
}));
});
});
Testing Skills
Unit Testing
// tests/skill.test.ts
import { describe, it, expect } from 'vitest';
import { startServer } from '../src/index';
describe('My Skill', () => {
it('should process tool correctly', async () => {
const server = await startServer();
const response = await server.callTool('my_tool', {
param1: 'test value',
param2: 42
});
expect(response.content[0].text).toContain('test value');
});
});
Integration Testing
Test skills with actual MCP clients:
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
const transport = new StdioClientTransport({
command: 'npx',
args: ['tsx', 'src/index.ts']
});
const client = new Client(
{ name: 'test-client' },
{ capabilities: { tools: {} } }
);
await client.connect(transport);
const result = await client.callTool('my_tool', {
param1: 'integration test'
});
console.log(result.content[0].text);
Deployment
Manual Installation
- Place skill in
apps/desktop/skills/directory - Install dependencies:
cd apps/desktop/skills/my-skill && npm install - Update
apps/desktop/package.json:"postinstall": "... && npm --prefix skills/my-skill install"
Development Mode
Run skill in development mode with hot reload:
cd apps/desktop/skills/my-skill
npm run dev
Best Practices
Security
- Input Validation: Always validate and sanitize inputs
- Error Handling: Don’t expose sensitive information in errors
- Rate Limiting: Implement for public APIs
- HTTPS: Use for external communications
Performance
- Keep Tools Focused: Each tool should do one thing well
- Lazy Loading: Only load heavy dependencies when needed
- Connection Pooling: For database/API connections
- Caching: For expensive operations
User Experience
- Clear Descriptions: Make tool purposes obvious
- Helpful Errors: Provide actionable error messages
- Progress Feedback: Use status updates for long operations
- Consistent Interfaces: Follow OpenWork conventions
Code Quality
- TypeScript: Use strict type checking
- Error Boundaries: Handle failures gracefully
- Logging: Use console.error for MCP logs
- Testing: Maintain comprehensive test coverage
Examples from OpenWork
Dev Browser Skill
- Complex MCP server with WebSocket relay
- Browser automation with state persistence
- ARIA snapshot element discovery
- Source:
apps/desktop/skills/dev-browser/src/index.ts
File Permission Skill
- Simple MCP tool implementation
- HTTP API integration with Electron
- User permission workflow
- Source:
apps/desktop/skills/file-permission/src/index.ts
Troubleshooting
Common Issues
Skill Not Detected
- Check directory structure is at
apps/desktop/skills/[skill-name]/ - Verify
package.jsonhas correct name and scripts - Ensure postinstall script runs
npm installfor the skill
MCP Connection Issues
- Confirm MCP SDK version is compatible
- Check console.error logs for startup messages
- Verify transport configuration (stdio vs HTTP)
Permission Problems
- Ensure file-permission skill is running
- Check HTTP endpoint is accessible
- Verify request format matches expected schema
Debug Mode Run skill with debug output:
DEBUG=* npm start
Source Files
This documentation is based on the OpenWork repository at commit 8855d5f31ca0dd485379af1ad241071fe62052a0.
Reference Implementations
apps/desktop/skills/dev-browser/- Browser automation exampleapps/desktop/skills/file-permission/- File management exampleapps/desktop/package.json- Installation scripts