Getting Started
Build your first Eney extension in minutes
Eney extensions (also referred to as skills) are MCP (Model Context Protocol) servers that expose interactive widgets to the Eney app. You write React components using the @eney/api widget library — the framework renders them as native macOS UI.
This guide walks you through creating a complete extension from scratch.
Prerequisites
- Node.js 24+
- npm
Setup
Clone the repository and run the setup script:
git clone https://github.com/MacPaw/eney-skills
cd eney-skills
./setup.shThis installs the CLI dependencies and registers the eney-skills-cli command globally. You only need to do this once after cloning the repository.
Create an Extension
Set up your extension
There are two ways to scaffold a new extension:
Option A: Using Claude Code
If you have Claude Code installed, use the /eney-create skill. It will scaffold the extension, implement the widget, and build it for you interactively:
/eney-createClaude Code will ask you for the extension details and handle the rest. You can skip ahead to Run in dev mode once it's done.
Option B: Using CLI directly
Scaffold a new extension manually:
eney-skills-cli create ...optionsExample call with all required options:
eney-skills-cli create --id weather-forecaster \
--mcp-title="Your town forecast" \
--tool-name="get-weather-forecast" \
--tool-description="Get Weather Forecast" \
--tool-title="Get Weather Forecast"This creates a new folder—located in the extensions/ folder by default—containing boilerplate code for your extension. It includes:
manifest.json- defines your MCP metadatapackage.json- your dependencies and scripts. NOTE: A build script is required for CLI commands to function.tsconfig.json- TypeScript configurationtests/- folder for your testscomponents/- folder for your React components (widgets)index.ts- entry point for your extension
Implement your widget
Using the tool-name we have provided during creation, the CLI has generated a boilerplate widget for us. It is located in the components/ folder.
import { useState } from "react";
import { z } from "zod";
import {
Action,
ActionPanel,
Form,
Paper,
defineWidget,
useCloseWidget,
} from "@eney/api";
const schema = z.object({
name: z.string().optional().describe("The name to greet."),
});
type Props = z.infer<typeof schema>;It is important to define a schema for your widget’s props. This allows Eney to understand the required input for the widget and provide a smoother experience for users.
function GetWeatherForecast(props: Props) {
const closeWidget = useCloseWidget();
const [name, setName] = useState(props.name ?? "");
const [result, setResult] = useState("");
function onSubmit() {
setResult(`Hello, ${name}!`);
}
function onDone() {
closeWidget("Done");
}closeWidget is used to finalize the interaction for the user and pass context back to Eney. Without calling it, the user’s only way to close the widget is by clicking the "X" button, which cancels the operation.
const actions = (
<ActionPanel>
<Action.SubmitForm title="Submit" onSubmit={onSubmit} style="secondary" />
<Action title="Done" onAction={onDone} style="primary" />
</ActionPanel>
);
return (
<Form actions={actions}>
{result && <Paper markdown={result} />}
<Form.TextField
name="name"
label="Name"
value={name}
onChange={setName}
/>
</Form>
);
}
const GetWeatherForecastWidget = defineWidget({
name: "get-weather-forecast",
description: "Get Weather Forecast",
schema,
component: GetWeatherForecast,
});
export default GetWeatherForecastWidget;defineWidget is the function that registers your widget and its metadata. Eney uses it to determine when to call the widget and which props to provide.
Verify it compiles
Run the build script to ensure your project is configured correctly:
npm run buildA successful build with no errors means your extension is ready to go.
Run in dev mode
The CLI dev command compiles, deploys locally, and watches for changes:
eney-skills-cli devThis command moves the compiled output to the Eney MCP folder and generates the tool definitions required for Eney to discover and load your widget.
Launch in Eney
Trigger the widget directly using a macOS deeplink:
open "eney://run?manifestID=eney_core&commandID=get-weather-forecast"The commandID matches the name field defined in your defineWidget() call (found in index.ts).
You can also find this specific open command printed in the terminal output of the dev command.
Next Steps
- Read Extension Structure to understand each file's role
- Browse the Widgets reference for all available UI components
- Study existing extensions in
extensions/for real-world patterns