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

  1. Place skill in apps/desktop/skills/ directory
  2. Install dependencies:
    cd apps/desktop/skills/my-skill && npm install
    
  3. 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

File Permission Skill

Troubleshooting

Common Issues

Skill Not Detected

  • Check directory structure is at apps/desktop/skills/[skill-name]/
  • Verify package.json has correct name and scripts
  • Ensure postinstall script runs npm install for 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


Back to top

OpenWork Documentation - Community documentation for accomplish-ai/openwork