원클릭으로
eney-create
Create a new Eney MCP skill using the CLI scaffolding tool, then implement widget components with Eney UIX.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Create a new Eney MCP skill using the CLI scaffolding tool, then implement widget components with Eney UIX.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Reference documentation for Eney UIX widgets and UI generation. Covers Form, Paper, Actions, and all components for building native macOS UIs in MCP extensions.
Set up SSH key commit signing for Git and GitHub so all commits (including those made via Claude Code) are cryptographically verified.
Build an Eney MCP skill, deploy it locally, launch it in the Eney app via deeplink, and iterate on issues with the user.
Add and run tests for an Eney MCP skill using the UIX test session API and node:test.
| name | eney-create |
| description | Create a new Eney MCP skill using the CLI scaffolding tool, then implement widget components with Eney UIX. |
| metadata | {"author":"macpaw","version":"3.0"} |
Eney skills are MCP (Model Context Protocol) servers that expose widgets via @eney/api. Each MCP runs as a standalone Node.js process communicating over stdio.
Make sure you have the CLI installed and linked globally. Run from the repo root:
npm run setup
This runs cd cli && npm install --no-fund --no-audit && npm link, installing CLI dependencies and making eney-skills-cli available globally.
If
npm run setupfails, run the commands manually:cd cli && npm install && npm link.
Ask the user for (skip what's already provided):
-mcp (e.g., color-converter-mcp)convert-color)Start from an up-to-date main:
git checkout main && git pull
git checkout -b feat/<mcp-id>
Run from the repo root:
eney-skills-cli create \
--id <mcp-id> \
--mcp-title "<MCP Title>" \
--tool-name <tool-name> \
--tool-description "<Tool Description>" \
--tool-title "<Tool Title>" \
-o ./extensions
If
eney-skills-cliis not found, runnpm run setupfrom the repo root first.
This creates the full MCP structure under extensions/<mcp-id>/ and installs base dependencies.
If the widget needs third-party libraries (e.g., qrcode, sharp, crypto-js), install them in the MCP directory:
cd extensions/<mcp-id> && npm install <package-name>
For packages with TypeScript types, also install the types:
npm install <package-name> @types/<package-name>
Edit extensions/<mcp-id>/components/<tool-name>.tsx.
Every widget follows this pattern:
import { useState } from "react";
import { z } from "zod";
import { Action, ActionPanel, Form, Paper, CardHeader, defineWidget, useCloseWidget } from "@macpaw/eney-api";
// 1. Schema — all fields need .describe() and should be .optional()
const schema = z.object({
input: z.string().optional().describe("The input value."),
});
type Props = z.infer<typeof schema>;
// 2. Component — uses Form, Paper, ActionPanel primitives (NO HTML elements)
function MyTool(props: Props) {
const closeWidget = useCloseWidget();
const [input, setInput] = useState(props.input ?? "");
const [result, setResult] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");
async function onSubmit() {
if (!input.trim()) return;
setIsLoading(true);
setError("");
try {
const output = await process(input);
setResult(output);
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
} finally {
setIsLoading(false);
}
}
function onDone() {
closeWidget("Done.");
}
// Result state — swap UI after operation
if (result) {
return (
<Form
header={<CardHeader title="My Tool" />}
actions={
<ActionPanel layout="row">
<Action.SubmitForm title="Start Over" onSubmit={() => setResult("")} style="secondary" />
<Action title="Done" onAction={onDone} style="primary" />
</ActionPanel>
}
>
<Paper markdown={result} />
<Form.TextField name="input" label="Input" value={input} onChange={setInput} isCopyable />
</Form>
);
}
// Input state
return (
<Form
header={<CardHeader title="My Tool" />}
actions={
<ActionPanel>
<Action.SubmitForm
title={isLoading ? "Processing..." : "Process"}
onSubmit={onSubmit}
style="primary"
isLoading={isLoading}
isDisabled={!input.trim()}
/>
</ActionPanel>
}
>
{error && <Paper markdown={`**Error:** ${error}`} />}
<Form.TextField name="input" label="Input" value={input} onChange={setInput} />
</Form>
);
}
// 3. defineWidget — wraps component for registration
const MyToolWidget = defineWidget({
name: "my-tool",
description: "Process the provided input",
schema,
component: MyTool,
});
export default MyToolWidget;
onChange is REQUIRED on all form fields — even display-only fields need it.Form.NumberField value is number | null, not number..js extensions for local imports: import X from "./foo.js".Form, Paper, ActionPanel, Files, CardHeader).<img> tags — display images via <Paper markdown="" /> or <Files><Files.Item path="..." /></Files>.| Field | Required Props | Optional Props | Notes |
|---|---|---|---|
Form.TextField | name, value, onChange | label, isCopyable | Single-line text |
Form.PasswordField | name, value, onChange | label | Masked input |
Form.NumberField | name, value, onChange | label, min, max | value is number | null |
Form.Checkbox | name, checked, onChange | label, variant | "checkbox" or "switch" |
Form.Dropdown | name, value, onChange | label, searchable | Children: Form.Dropdown.Item |
Form.DatePicker | name, value, onChange | label, type | "date", "time", "datetime" |
Form.FilePicker | name, value, onChange | label, accept, multiple | File selection dialog |
Form.RichTextEditor | value, onChange | isInitiallyFocused | Rich text area |
| Action | Key Props | Notes |
|---|---|---|
Action | title, onAction, style, isLoading, isDisabled | Generic button |
Action.SubmitForm | title, onSubmit, style, isLoading, isDisabled | Form submit |
Action.CopyToClipboard | content, title | Copy text to clipboard |
Action.ShowInFinder | path, title | Reveal file in Finder |
Create additional files in components/ and register each in index.ts:
import WidgetA from "./components/widget-a.js";
import WidgetB from "./components/widget-b.js";
uixServer.registerWidget(WidgetA);
uixServer.registerWidget(WidgetB);
cd extensions/<mcp-id> && npm run build
Always use
npm run buildto verify — nevernpx tsc --noEmitornpx tscdirectly.
| Error | Cause | Fix |
|---|---|---|
Property 'onChange' is missing | onChange is required on all form fields | Add onChange={setter} even for display-only fields |
Type 'number' is not assignable to 'number | null' | NumberField value is nullable | Use useState<number | null>(defaultValue) |
Cannot find module './foo' | Missing .js extension in import | Use import X from "./foo.js" |
To publish the extension, create a pull request to main with a short description of what the extension does:
git add extensions/<mcp-id>
git commit -m "feat: add <mcp-id>"
git push -u origin feat/<mcp-id>
gh pr create --title "feat: add <MCP Title>" --body "Short description of the extension."