Skip to main content
Back to Blog
AI Tools

How to Build Custom Claude Cowork Plugins

Learn how to build custom Claude Cowork plugins. Complete guide with architecture, development, testing, and distribution.

Keyur Patel
Keyur Patel
February 20, 2026
•
12 min read

Introduction: Creating Your Custom Plugin

Learning to build cowork plugins gives you the power to extend Claude's capabilities beyond the marketplace. While the Claude Cowork plugin marketplace includes hundreds of integrations, you sometimes need a plugin tailored to your specific system or workflow. Whether you're integrating a proprietary tool, adding functionality to an internal system, or creating specialized capabilities for your industry, building a custom plugin gives you complete control.

This guide walks through the complete process of building a Claude Cowork plugin from conception through distribution. You'll learn the plugin architecture, development process, testing strategies, packaging, and how to share your plugin with others.

Understanding Plugin Architecture

Before writing code, understand how Claude Cowork plugins work.

The Model Context Protocol (MCP) Foundation

MCPs are the standardized protocol Claude Cowork uses to communicate with external systems. If you want a deeper dive into the protocol itself, see our guide on MCP and the Model Context Protocol explained. An MCP server:

  • Exposes resources: Information sources Claude can query (files, databases, APIs)
  • Provides tools: Functions Claude can invoke (create, modify, delete, execute)
  • Emits prompts: Template instructions that guide Claude's behavior
  • Manages capabilities: Defines what a plugin can and cannot do
MCPs work over standard protocols (JSON-RPC over HTTP/WebSocket, stdio), making them language and framework agnostic.

Plugin Structure

A complete Claude Cowork plugin consists of:

your-plugin/
├── plugin.yaml              # Plugin manifest
├── SKILL.md                 # Skill definitions
├── src/
│   ├── server.ts           # MCP server implementation
│   ├── tools/              # Tool implementations
│   │   ├── read.ts
│   │   ├── write.ts
│   │   └── query.ts
│   ├── resources/          # Resource definitions
│   └── prompts/            # Claude instruction prompts
├── tests/                   # Test suite
├── dist/                    # Compiled output
├── package.json            # Node.js package manifest
├── tsconfig.json           # TypeScript configuration
└── README.md               # Documentation

Key Concepts: Tools, Resources, and Prompts

Tools are functions Claude can call:

json
{
  "name": "query_database",
  "description": "Query the custom database",
  "inputSchema": {
    "type": "object",
    "properties": {
      "query": { "type": "string" },
      "limit": { "type": "number" }
    },
    "required": ["query"]
  }
}

Resources are data sources Claude can read:

json
{
  "uri": "plugin://my-system/projects/123",
  "mimeType": "application/json",
  "contents": { "projectData": "..." }
}

Prompts are Claude instruction templates:

markdown
# Database Query Assistant

You have access to a custom database containing...
Use the query_database tool to fetch information.
Always validate queries before executing them.

Prerequisites and Project Setup

System Requirements

  • Node.js: Version 18+ (we'll use TypeScript)
  • Package Manager: npm or yarn
  • Development Environment: VS Code or similar
  • Claude Cowork: Installed locally for testing
  • Understanding: Basic TypeScript and async/await patterns

Creating Project Scaffold

Create a new TypeScript project:

bash
# Create project directory
mkdir cowork-project-manager-plugin
cd cowork-project-manager-plugin

# Initialize Node project
npm init -y

# Install dependencies
npm install typescript @types/node
npm install @anthropic-ai/sdk
npm install @types/express express
npm install -D ts-node @types/jest jest

# Create TypeScript configuration
npx tsc --init

Configuring TypeScript

Create tsconfig.json:

json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "moduleResolution": "node"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "tests", "dist"]
}

Creating the Plugin Manifest

The plugin manifest (plugin.yaml) declares your plugin to Claude Cowork.

yaml
# Plugin metadata
name: "project-manager-plugin"
version: "1.0.0"
author: "Your Name"
description: "Integrate with internal project management system"

# Display information
displayName: "Project Manager"
icon: "icon.png"
category: "Productivity"

# Plugin capabilities
capabilities:
  - "tool:execute"      # Can execute tools
  - "resource:read"     # Can read resources
  - "resource:write"    # Can write resources
  - "prompt:template"   # Provides prompt templates

# Required configuration
requiredConfig:
  - name: "API_KEY"
    type: "string"
    description: "API key for project management system"
    secret: true
  - name: "API_URL"
    type: "string"
    description: "Base URL for API"

# Optional configuration
optionalConfig:
  - name: "MAX_RESULTS"
    type: "number"
    description: "Maximum results per query"
    default: 100

# Plugin permissions
permissions:
  - "system:read_files"
  - "system:write_files"
  - "network:http"

# Supported Claude Cowork versions
minVersion: "1.0.0"
maxVersion: "*"

# Plugin dependencies
dependencies:
  - name: "nodejs"
    version: ">=18.0.0"

# Entry point
entryPoint: "dist/server.js"

# Health check endpoint
healthCheck:
  endpoint: "/health"
  interval: 60000
  timeout: 5000

Building the MCP Server

The MCP server is your plugin's core. It implements the server logic that Claude Cowork communicates with.

Basic Server Implementation

Create src/server.ts:

typescript
import Anthropic from "@anthropic-ai/sdk";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import {
  StdioServerTransport,
} from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  Tool,
  TextContent,
  McpError,
} from "@modelcontextprotocol/sdk/types.js";

// Initialize MCP server
const server = new Server({
  name: "project-manager-plugin",
  version: "1.0.0",
});

// Configuration from environment
const API_KEY = process.env.API_KEY || "";
const API_URL = process.env.API_URL || "https://api.example.com";

// Validate configuration
if (!API_KEY || !API_URL) {
  console.error("Missing required configuration");
  process.exit(1);
}

/**
 * Tool: Query projects
 * Searches the project management system
 */
const queryProjectsTool: Tool = {
  name: "query_projects",
  description:
    "Search for projects in the project management system",
  inputSchema: {
    type: "object" as const,
    properties: {
      search: {
        type: "string",
        description: "Search query (project name or keywords)",
      },
      status: {
        type: "string",
        enum: ["active", "completed", "archived", "all"],
        description: "Filter by project status",
      },
      limit: {
        type: "number",
        description: "Maximum number of results (default: 10)",
      },
    },
    required: ["search"],
  },
};

/**
 * Tool: Create project
 * Creates a new project in the system
 */
const createProjectTool: Tool = {
  name: "create_project",
  description: "Create a new project in the project management system",
  inputSchema: {
    type: "object" as const,
    properties: {
      name: {
        type: "string",
        description: "Project name",
      },
      description: {
        type: "string",
        description: "Project description",
      },
      startDate: {
        type: "string",
        description: "Project start date (YYYY-MM-DD)",
      },
      dueDate: {
        type: "string",
        description: "Project due date (YYYY-MM-DD)",
      },
      owner: {
        type: "string",
        description: "Project owner email or user ID",
      },
    },
    required: ["name", "owner"],
  },
};

/**
 * Tool: Update project
 * Updates an existing project
 */
const updateProjectTool: Tool = {
  name: "update_project",
  description: "Update a project in the project management system",
  inputSchema: {
    type: "object" as const,
    properties: {
      projectId: {
        type: "string",
        description: "Project ID",
      },
      updates: {
        type: "object",
        description:
          "Fields to update (name, description, status, dueDate, owner)",
      },
    },
    required: ["projectId", "updates"],
  },
};

/**
 * Tool: List tasks
 * Lists tasks within a project
 */
const listTasksTool: Tool = {
  name: "list_tasks",
  description: "List tasks for a specific project",
  inputSchema: {
    type: "object" as const,
    properties: {
      projectId: {
        type: "string",
        description: "Project ID",
      },
      status: {
        type: "string",
        enum: ["pending", "in_progress", "completed", "all"],
        description: "Filter by task status",
      },
      assignee: {
        type: "string",
        description: "Filter by assignee (optional)",
      },
    },
    required: ["projectId"],
  },
};

/**
 * Tool: Create task
 * Creates a new task in a project
 */
const createTaskTool: Tool = {
  name: "create_task",
  description: "Create a new task in a project",
  inputSchema: {
    type: "object" as const,
    properties: {
      projectId: {
        type: "string",
        description: "Project ID",
      },
      title: {
        type: "string",
        description: "Task title",
      },
      description: {
        type: "string",
        description: "Task description",
      },
      assignee: {
        type: "string",
        description: "Assignee email",
      },
      dueDate: {
        type: "string",
        description: "Due date (YYYY-MM-DD)",
      },
      priority: {
        type: "string",
        enum: ["low", "medium", "high"],
        description: "Task priority",
      },
    },
    required: ["projectId", "title"],
  },
};

// Register tools with server
server.setRequestHandler(RequestedToolList, async () => ({
  tools: [
    queryProjectsTool,
    createProjectTool,
    updateProjectTool,
    listTasksTool,
    createTaskTool,
  ],
}));

// Tool execution handler
server.setRequestHandler(CallTool, async (request) => {
  const { name, arguments: args } = request.params;

  try {
    switch (name) {
      case "query_projects":
        return await handleQueryProjects(args);
      case "create_project":
        return await handleCreateProject(args);
      case "update_project":
        return await handleUpdateProject(args);
      case "list_tasks":
        return await handleListTasks(args);
      case "create_task":
        return await handleCreateTask(args);
      default:
        return {
          content: [
            {
              type: "text" as const,
              text: `Unknown tool: ${name}`,
            },
          ],
          isError: true,
        };
    }
  } catch (error) {
    return {
      content: [
        {
          type: "text" as const,
          text: `Error executing tool: ${error instanceof Error ? error.message : String(error)}`,
        },
      ],
      isError: true,
    };
  }
});

// Tool implementation handlers
async function handleQueryProjects(args: Record<string, unknown>) {
  const { search, status = "all", limit = 10 } = args;

  // Call your actual API
  const response = await fetch(`${API_URL}/projects/search`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${API_KEY}`,
    },
    body: JSON.stringify({ search, status, limit }),
  });

  if (!response.ok) {
    throw new Error(`API error: ${response.statusText}`);
  }

  const data = await response.json();

  return {
    content: [
      {
        type: "text" as const,
        text: JSON.stringify(data, null, 2),
      },
    ],
  };
}

async function handleCreateProject(args: Record<string, unknown>) {
  const { name, description, startDate, dueDate, owner } = args;

  const response = await fetch(`${API_URL}/projects`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${API_KEY}`,
    },
    body: JSON.stringify({
      name,
      description,
      startDate,
      dueDate,
      owner,
    }),
  });

  if (!response.ok) {
    throw new Error(`API error: ${response.statusText}`);
  }

  const data = await response.json();

  return {
    content: [
      {
        type: "text" as const,
        text: `Project created successfully: ${JSON.stringify(data)}`,
      },
    ],
  };
}

async function handleUpdateProject(args: Record<string, unknown>) {
  const { projectId, updates } = args;

  const response = await fetch(`${API_URL}/projects/${projectId}`, {
    method: "PATCH",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${API_KEY}`,
    },
    body: JSON.stringify(updates),
  });

  if (!response.ok) {
    throw new Error(`API error: ${response.statusText}`);
  }

  const data = await response.json();

  return {
    content: [
      {
        type: "text" as const,
        text: `Project updated successfully: ${JSON.stringify(data)}`,
      },
    ],
  };
}

async function handleListTasks(args: Record<string, unknown>) {
  const { projectId, status = "all", assignee } = args;

  const params = new URLSearchParams({
    status: status as string,
    ...(assignee && { assignee: assignee as string }),
  });

  const response = await fetch(
    `${API_URL}/projects/${projectId}/tasks?${params}`,
    {
      headers: {
        Authorization: `Bearer ${API_KEY}`,
      },
    }
  );

  if (!response.ok) {
    throw new Error(`API error: ${response.statusText}`);
  }

  const data = await response.json();

  return {
    content: [
      {
        type: "text" as const,
        text: JSON.stringify(data, null, 2),
      },
    ],
  };
}

async function handleCreateTask(args: Record<string, unknown>) {
  const { projectId, title, description, assignee, dueDate, priority } = args;

  const response = await fetch(`${API_URL}/projects/${projectId}/tasks`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${API_KEY}`,
    },
    body: JSON.stringify({
      title,
      description,
      assignee,
      dueDate,
      priority,
    }),
  });

  if (!response.ok) {
    throw new Error(`API error: ${response.statusText}`);
  }

  const data = await response.json();

  return {
    content: [
      {
        type: "text" as const,
        text: `Task created successfully: ${JSON.stringify(data)}`,
      },
    ],
  };
}

// Start the server
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Project Manager Plugin MCP server started");
}

main().catch(console.error);

Defining Skills

Skills are the user-facing interface to your plugin. Create SKILL.md:

markdown
# Project Manager Plugin Skills

## Query Projects

**Description**: Search for projects in your project management system.

**Usage**:

Query projects for "Marketing" with active status.

**Parameters**:
- `search` (required): Project name or keywords to search
- `status` (optional): Filter by status (active, completed, archived, all)
- `limit` (optional): Maximum results to return (default: 10)

**Example Output**:
```json
{
  "projects": [
    {
      "id": "proj_123",
      "name": "Q2 Marketing Campaign",
      "status": "active",
      "owner": "jane@company.com",
      "progress": 65,
      "dueDate": "2026-06-30"
    }
  ]
}

Create Project

Description: Create a new project in your project management system.

Usage:

Create a new project called "Website Redesign" due June 30, 2026.

Parameters:

  • name (required): Project name
  • description (optional): Detailed description
  • startDate (optional): Project start date (YYYY-MM-DD)
  • dueDate (optional): Project due date (YYYY-MM-DD)
  • owner (required): Project owner email
Example Output:

json
{
  "id": "proj_456",
  "name": "Website Redesign",
  "created": "2026-02-20",
  "owner": "john@company.com"
}

Update Project

Description: Update an existing project's details.

Usage:

Update project "Website Redesign" to mark it 75% complete.

Parameters:

  • projectId (required): Project ID
  • updates (required): Fields to update

List Tasks

Description: List all tasks in a project.

Usage:

Show me all pending tasks in the Q2 Marketing Campaign.

Parameters:

  • projectId (required): Project ID
  • status (optional): Filter by status
  • assignee (optional): Filter by assignee

Create Task

Description: Create a new task within a project.

Usage:

Add a task to the Website Redesign project: "Update homepage design" due March 15.

Parameters:

  • projectId (required): Project ID
  • title (required): Task title
  • description (optional): Task details
  • assignee (optional): Assignee email
  • dueDate (optional): Due date
  • priority (optional): Task priority (low, medium, high)
## Building and Testing Locally

### Compiling TypeScript

```bash
npm run build

Running Tests

Create tests/server.test.ts:

typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";

describe("Project Manager Plugin", () => {
  it("should query projects successfully", async () => {
    // Test implementation
    expect(true).toBe(true);
  });

  it("should create projects with required fields", async () => {
    // Test implementation
    expect(true).toBe(true);
  });

  it("should handle API errors gracefully", async () => {
    // Test implementation
    expect(true).toBe(true);
  });
});

Run tests:

bash
npm test

Local Testing with Claude Cowork

  • Build the plugin: npm run build
  • Create test directory: mkdir -p ~/.cowork/plugins/project-manager
  • Copy files: cp -r dist/* ~/.cowork/plugins/project-manager/
  • Update environment: Create .env file with API_KEY and API_URL
  • Restart Cowork: Launch Claude Cowork
  • Load plugin: In Cowork, go to Plugins > Developer > Load Local Plugin
  • Configure: Provide API credentials
  • Test: Ask Cowork to use your plugin
Example test conversation:

You: "Search for projects related to marketing"
Claude: [Uses project-manager plugin to query projects]

You: "Create a new project called 'Brand Refresh' for the marketing team"
Claude: [Uses create_project tool]

You: "What tasks are pending in the Website Redesign project?"
Claude: [Uses list_tasks tool to show pending items]

Packaging Your Plugin

Creating Distribution Package

bash
# Create distribution directory
mkdir dist-plugin

# Copy essential files
cp plugin.yaml dist-plugin/
cp SKILL.md dist-plugin/
cp dist/* dist-plugin/
cp package.json dist-plugin/
cp package-lock.json dist-plugin/
cp README.md dist-plugin/

# Create plugin archive
tar -czf project-manager-plugin-1.0.0.tar.gz dist-plugin/

Plugin File Format

For direct installation, create a .plugin file:

bash
zip project-manager-plugin.plugin 
  plugin.yaml 
  SKILL.md 
  dist/server.js 
  dist/server.js.map 
  package.json 
  package-lock.json 
  README.md

Versioning and Updates

Update plugin.yaml for each release:

yaml
name: "project-manager-plugin"
version: "1.1.0"  # Changed from 1.0.0

Document changes in changelog:

markdown
# Changelog

## 1.1.0 (2026-03-01)
- Added bulk task creation
- Improved error handling
- Fixed project status filtering
- Performance optimizations

## 1.0.0 (2026-02-20)
- Initial release
- Core project and task management
- Search and filtering capabilities

Best Practices for Plugin Development

Error Handling

Always implement robust error handling:

typescript
try {
  // Plugin logic
} catch (error) {
  console.error("Plugin error:", error);
  return {
    content: [
      {
        type: "text" as const,
        text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`,
      },
    ],
    isError: true,
  };
}

Rate Limiting and Throttling

Implement rate limiting for external APIs:

typescript
const rateLimiter = new Map<string, number>();
const MAX_REQUESTS_PER_MINUTE = 60;

function checkRateLimit(key: string): boolean {
  const now = Date.now();
  const requests = rateLimiter.get(key) || 0;

  if (requests > MAX_REQUESTS_PER_MINUTE) {
    return false;
  }

  rateLimiter.set(key, requests + 1);
  return true;
}

Logging

Implement structured logging:

typescript
function log(level: string, message: string, data?: unknown) {
  console.error(
    JSON.stringify({
      timestamp: new Date().toISOString(),
      level,
      message,
      ...(data && { data }),
    })
  );
}

Security

  • Validate inputs: All parameters from Claude
  • Sanitize outputs: Before returning to Claude
  • Secure credentials: Never log API keys
  • Use HTTPS: For API communication
  • Implement timeouts: Prevent hanging requests

Distributing Your Plugin

Submitting to Marketplace

To get your plugin in the official marketplace:

  • Create account: Register on the Cowork developer portal
  • Prepare documentation: Comprehensive README and usage guide
  • Set pricing (if applicable): Free or paid model
  • Submit for review: Anthropic team reviews for security, quality, and documentation
  • Publish: Upon approval, your plugin appears in the marketplace

Community Sharing

Share your plugin on:

  • GitHub: Open-source plugin repository
  • npm: Distribute as npm package
  • Community forums: Announce on Cowork community
  • Plugin registry: Community-maintained plugin registry

Example: Complete Project Management Plugin

A complete, production-ready project management plugin includes:

  • Comprehensive MCP server with all CRUD operations
  • Detailed SKILL.md with all capabilities documented
  • Plugin manifest with metadata and permissions
  • Full test coverage
  • Error handling and rate limiting
  • README with installation and usage instructions
  • Version management and changelog
The example throughout this guide provides the foundation. Build upon it with:

  • Authentication plugins (OAuth, API key management)
  • Caching layers for performance
  • Webhook handlers for real-time updates
  • Advanced filtering and search
  • Bulk operations for efficiency

Troubleshooting Plugin Development

Plugin Not Appearing in Claude Cowork

  • Check plugin.yaml syntax (YAML format errors)
  • Verify entryPoint path matches compiled output
  • Ensure TypeScript compilation successful (npm run build)
  • Check Claude Cowork logs for loading errors
  • Restart Claude Cowork application

Tools Not Executing

  • Verify tool handler implemented for the tool name
  • Check inputSchema matches arguments Claude provides
  • Validate API credentials and permissions
  • Check network connectivity to external APIs
  • Review error logs for specific failure reason

API Communication Issues

  • Verify API URL and endpoint correctness
  • Check API credentials (API key, OAuth token)
  • Test API directly with curl or Postman
  • Implement and check request timeout handling
  • Add detailed logging for API interactions

The Future of Custom Plugins

As the Cowork ecosystem matures, expect:

  • Plugin templates: Starter templates for common plugin types
  • Plugin builder GUI: Visual plugin builder for non-developers
  • Community marketplace: User-created, community-vetted plugins
  • Revenue sharing: Plugin developers earn from paid plugins
  • Advanced capabilities: Real-time collaboration, streaming responses
  • Mobile support: Plugins working across desktop and mobile

Conclusion: Building the Future

Creating custom Claude Cowork plugins lets you extend the platform to your exact needs. Whether integrating proprietary systems, adding industry-specific functionality, or creating niche solutions, plugin development puts the power of customization in your hands.

The combination of Claude's intelligence, Cowork's framework, and your specialized domain knowledge creates opportunities for powerful, innovative solutions that solve real problems.

[Build your first Cowork Plugin and share it with the community, and start developing today]

Keep Reading

Keyur Patel

Written by Keyur Patel

AI Engineer & Founder

Keyur Patel is the founder of AiPromptsX and an AI engineer with extensive experience in prompt engineering, large language models, and AI application development. After years of working with AI systems like ChatGPT, Claude, and Gemini, he created AiPromptsX to share effective prompt patterns and frameworks with the broader community. His mission is to democratize AI prompt engineering and help developers, content creators, and business professionals harness the full potential of AI tools.

Prompt EngineeringAI DevelopmentLarge Language ModelsSoftware Engineering

Explore Related Frameworks

Try These Related Prompts