How to Extend the Functionality of OpenWork: A Complete Plugin Development Guide
You can extend OpenWork by creating Markdown-based plugins, adding custom UI components in React, or implementing new MCP actions—all without modifying the core Electron application.
OpenWork is a modular AI workspace platform built on a plugin-first architecture. According to the different-ai/openwork source code, every capability—from simple skills to complex enterprise integrations—flows through the same Model-Control-Plane (MCP) protocol. This design lets you add new functionality by dropping a Markdown file into the plugins/ folder or extending the typed TypeScript APIs in packages/types/src/.
Understanding OpenWork's Extension Architecture
Before writing code, you need to understand how the five layers communicate. Each layer is isolated and typed, so changes in one rarely break others.
| Layer | Purpose | Key Source Files |
|---|---|---|
| Desktop front-end (Electron + React) | Renders UI, talks to Den via CDP | packages/ui/src/react/platform-detect.ts, packages/ui/src/common/paper.ts |
| Typed API contracts | Defines data shapes between layers | packages/types/src/den/mcp-connection-action.ts, packages/types/src/openwork-context.ts |
| MCP client | Thin library for agents to talk to OpenWork | packages/enterprise-mcp-client/src/enterprise-mcp-client.ts |
| MCP mock server | CI/E2E tests and protocol reference | packages/enterprise-mcp-mock-server/src/protocol/mcp-handler.ts |
| Skill/Plugin definition | Markdown files describing capabilities | packages/enterprise-mcp-client/src/tool-catalog.ts |
The flow works like this: an AI agent calls the MCP client, which forwards to the Den backend (Node/Express). The Den parses plugin definitions using tool-catalog.ts and returns capabilities. The desktop UI optionally renders components that call the same MCP endpoints.
Method 1: Create a Plugin Using Markdown (No Code Required)
The fastest way to extend OpenWork functionality is adding a skill definition—a Markdown file with YAML front-matter. The tool-catalog.ts scanner discovers these automatically.
Step 1: Write the Plugin File
Create a file in the plugins/ folder (or any watched directory):
---
name: My Cool Skill
description: Returns a random joke from an external API.
skill: my_cool_joke
---
{{#fetch "https://official-joke-api.appspot.com/jokes/random"}}
{{json .setup}} – {{json .punchline}}
{{/fetch}}
The skill key in front-matter registers my_cool_joke as an exposed tool. The template body uses OpenWork's built-in fetch helper to call external APIs.
Step 2: Start the Development Server
Run the watcher to rebuild the MCP catalog automatically:
pnpm dev
# or for isolated worktrees
pnpm dev:worktree
The enterprise-mcp-client rescans plugins/ on every change. No restart required.
Step 3: Test with an AI Agent
From any MCP-enabled agent (Claude Code, Cursor, OpenCode):
search_capabilities
The response includes my_cool_joke. Execute it:
execute_capability my_cool_joke
The agent receives the rendered template output.
Method 2: Add Custom UI Components in React
To surface your plugin in the desktop app, extend the React layer in packages/ui/src/react/.
Step 1: Create the Component
// packages/ui/src/react/my-cool-joke.tsx
import { useQuery } from '@tanstack/react-query';
import { executeCapability } from '@/connect-link';
export const MyCoolJoke = () => {
const { data, error, isLoading } = useQuery('joke', () =>
executeCapability('my_cool_joke')
);
if (isLoading) return <p>Loading…</p>;
if (error) return <p>Failed: {(error as Error).message}</p>;
return <blockquote className="p-4 bg-gray-100">{data}</blockquote>;
};
The executeCapability wrapper is typed against packages/types/src/den/mcp-connection-action.ts, ensuring compile-time safety for payload shapes.
Step 2: Export the Component
Update the barrel file:
// packages/ui/src/react/index.ts
export * from './my-cool-joke';
Step 3: Platform-Conditional Rendering (Optional)
Use platform-detect.ts to enable features only on specific operating systems:
import { isMacOS, isWindows } from './platform-detect';
const showJokeWidget = isMacOS(); // or your own logic
Method 3: Implement Custom MCP Actions
For deeper integrations, extend the protocol itself by adding new action types.
Define the Action Contract
Add your action to the typed schema:
// packages/types/src/den/mcp-connection-action.ts
export type MCPConnectionAction =
| { type: 'install'; mcpId: string }
| { type: 'grant'; mcpId: string; scope: string }
| { type: 'archive'; mcpId: string }
| { type: 'my_custom_action'; payload: MyCustomPayload }; // your extension
Implement the Handler
Reference mcp-handler.ts for the routing pattern:
// packages/enterprise-mcp-mock-server/src/protocol/mcp-handler.ts (reference)
async function handleAction(action: MCPConnectionAction): Promise<Result> {
switch (action.type) {
case 'my_custom_action':
return await handleCustomAction(action.payload);
// ...
}
}
The mock server serves as both test infrastructure and reference implementation for protocol extensions.
Method 4: Expose OAuth and Credentials
Plugins often need authenticated API access. Study oauth-provider.ts for the credential flow:
// packages/enterprise-mcp-client/src/oauth-provider.ts
export class OAuthProvider {
async getCredentials(skillName: string): Promise<OAuthToken> {
// Retrieves stored tokens from the Den backend
}
}
Your plugin's Markdown front-matter can declare required OAuth scopes, and the MCP client handles the consent flow before executing the skill.
Configuration Reference
All MCP endpoints and plugin sources are declared in opencode.json:
{
"mcp": {
"openwork": {
"command": "node",
"args": ["./packages/enterprise-mcp-client/dist/index.js"],
"env": {
"OPENWORK_API_KEY": "${OPENWORK_API_KEY}"
}
}
}
}
Agents read this file to discover available capabilities. The mcp section points to your local or remote Den instance.
Key Extension Points in the Source Code
| File | Purpose | When to Modify |
|---|---|---|
packages/enterprise-mcp-client/src/enterprise-mcp-client.ts |
Core MCP client library | Adding protocol features |
packages/enterprise-mcp-client/src/tool-catalog.ts |
Plugin discovery | Changing how skills are found |
packages/types/src/den/mcp-connection-action.ts |
Action type definitions | New MCP operations |
packages/types/src/openwork-context.ts |
Global plugin context | Exposing new runtime data |
packages/ui/src/react/platform-detect.ts |
OS feature flags | UI per-platform behavior |
packages/ui/src/react/index.ts |
Component exports | New UI components |
opencode.json |
MCP configuration | Adding/removing endpoints |
Summary
- Markdown plugins (
plugins/*.md) add capabilities instantly—no build step, no code changes. - React components in
packages/ui/src/react/surface plugins in the desktop UI with full TypeScript safety. - MCP actions extend the protocol by adding types to
mcp-connection-action.tsand handlers followingmcp-handler.tspatterns. - OAuth flows reuse
oauth-provider.tsfor secure credential management. - Every layer respects the contracts in
packages/types/src/, preventing breaking changes across the stack.
Frequently Asked Questions
Can I extend OpenWork without writing TypeScript?
Yes. Create a Markdown file in plugins/ with YAML front-matter declaring name, description, and skill. The tool-catalog.ts scanner picks it up automatically. Template helpers like {{#fetch}} handle HTTP calls without custom code.
How do I test my plugin before deploying?
Run pnpm dev to start the file watcher, then use any MCP-enabled agent (Claude Code, Cursor, etc.) with search_capabilities and execute_capability commands. For automated testing, reference the mock server in packages/enterprise-mcp-mock-server/src/protocol/mcp-handler.ts.
What limits exist on plugin functionality?
Plugins can call external APIs via template helpers, access OAuth credentials through oauth-provider.ts, and receive context from openwork-context.ts. They cannot directly modify the Electron main process or Den backend without contributing to those packages—this is intentional for security isolation.
Where should I place custom UI components?
Create files in packages/ui/src/react/ and re-export them from packages/ui/src/react/index.ts. Components use platform-detect.ts to conditionally render based on the operating system, ensuring consistent behavior across Windows, macOS, and Linux.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →