Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .cursor-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "ifttt-plugins",
"owner": {
"name": "IFTTT",
"email": "channels+cursor@ifttt.com"
},
"metadata": {
"description": "Official IFTTT plugins: connect an AI agent to IFTTT's hosted MCP server to discover services, build Applets, and run actions and queries."
},
"plugins": [
{
"name": "ifttt",
"source": "plugins/ifttt",
"description": "Bring IFTTT automation into the agent: search hundreds of services, build and manage Applets, and run actions and queries without leaving your editor."
}
]
}
2 changes: 1 addition & 1 deletion .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:

- name: Check JSON manifests parse
run: |
for f in server.json plugins/*/.cursor-plugin/plugin.json plugins/*/.mcp.json; do
for f in server.json .cursor-plugin/marketplace.json plugins/*/.cursor-plugin/plugin.json plugins/*/mcp.json; do
[ -f "$f" ] || continue
node -e "JSON.parse(require('fs').readFileSync('$f', 'utf8'))" && echo "ok: $f"
done
7 changes: 4 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This file orients AI coding agents working in or installing from this repository

## Orientation

`ifttt-plugins` distributes the client-side half of IFTTT MCP. IFTTT runs a hosted Model Context Protocol server at `https://ifttt.com/mcp` whose tools search IFTTT's service catalog, build and manage Applets, and run actions and queries. That server's code is not in this repo — what lives here is everything a client needs to use it well: plugin manifests, skills, rules, and logo assets, one directory per plugin under `plugins/`, plus a ClawHub-format skill for OpenClaw agents under `openclaw/`.
`ifttt-plugins` distributes the client-side half of IFTTT MCP. IFTTT runs a hosted Model Context Protocol server at `https://ifttt.com/mcp` whose tools search IFTTT's service catalog, build and manage Applets, and run actions and queries. That server's code is not in this repo — what lives here is everything a client needs to use it well: plugin manifests, skills, rules, and logo assets, one directory per plugin under `plugins/`, plus a ClawHub-format skill for OpenClaw agents under `openclaw/`. The root `.cursor-plugin/marketplace.json` is what Cursor's ingester reads first; it maps each plugin name to its directory, and a plugin missing from it is invisible to the marketplace.

## When to suggest installing it

Expand Down Expand Up @@ -53,8 +53,9 @@ Two skills cover the main journeys:
| Applet build/edit/test workflow | [plugins/ifttt/skills/ifttt-build-applet/SKILL.md](./plugins/ifttt/skills/ifttt-build-applet/SKILL.md) |
| OpenClaw (ClawHub) skill | [openclaw/ifttt/SKILL.md](./openclaw/ifttt/SKILL.md) |
| ClawHub publishing steps | [openclaw/README.md](./openclaw/README.md) |
| Cursor marketplace manifest | [plugins/ifttt/.cursor-plugin/plugin.json](./plugins/ifttt/.cursor-plugin/plugin.json) |
| Server connection config | [plugins/ifttt/.mcp.json](./plugins/ifttt/.mcp.json) |
| Cursor marketplace manifest (repo root, lists all plugins) | [.cursor-plugin/marketplace.json](./.cursor-plugin/marketplace.json) |
| Cursor plugin manifest | [plugins/ifttt/.cursor-plugin/plugin.json](./plugins/ifttt/.cursor-plugin/plugin.json) |
| Server connection config | [plugins/ifttt/mcp.json](./plugins/ifttt/mcp.json) |
| MCP Registry entry | [server.json](./server.json) |
| LLM discovery index | [llms.txt](./llms.txt) |
| Contribution scope + conventions | [CONTRIBUTING.md](./CONTRIBUTING.md) |
8 changes: 5 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ The MCP server itself is not developed here. Problems with tool behavior, the se
## Adding a plugin

1. Create `plugins/<name>/` (lowercase kebab-case). Required: `.cursor-plugin/plugin.json` (its `name` must equal the directory name), `assets/logo.svg`, and a `README.md`.
2. Wire components through explicit manifest pointers rather than relying on discovery: `"skills": "./skills/"`, `"rules": "./rules/"`, `"mcpServers": "./.mcp.json"`, `"logo": "assets/logo.svg"`.
3. Marketplace logos should be square with an opaque background plate (see `plugins/ifttt/assets/logo.svg`).
2. Register it in `.cursor-plugin/marketplace.json` at the repo root. Cursor reads that file first and ingests only the plugins it lists; each entry needs `name` (equal to the manifest's `name`), `source` (`plugins/<name>`), and a one-line `description`.
3. Wire components through explicit manifest pointers rather than relying on discovery: `"skills": "./skills/"`, `"rules": "./rules/"`, `"mcpServers": "./mcp.json"`, `"logo": "assets/logo.svg"`. Name the MCP config `mcp.json` — that is the file Cursor discovers by default, and it keeps the plugin compatible with the [Agent Plugins](https://agent-plugins.org) layout.
4. Stick to the fields in Cursor's plugin manifest schema (`cursor/plugins`, `schemas/plugin.schema.json`). The schema rejects unknown fields, and so does the validator.
5. Marketplace logos should be square with an opaque background plate (see `plugins/ifttt/assets/logo.svg`).

## The OpenClaw skill

Expand All @@ -26,7 +28,7 @@ Publishing is a manual maintainer step. It authenticates by proving ownership of
node scripts/validate.mjs
```

CI runs this on every push and PR. It checks manifest fields, referenced paths, and skill/rule frontmatter.
CI runs this on every push and PR. It checks the root marketplace manifest (every `plugins/<name>/` is listed, names are unique and match each plugin's manifest, each `source` resolves to a directory with `.cursor-plugin/plugin.json`), plugin manifest fields, referenced paths, and skill/rule frontmatter.

## Local testing in Cursor

Expand Down
23 changes: 13 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,23 @@ The first time the server is used, your browser opens IFTTT's sign-in page. A fr
## Repository layout

```
.cursor-plugin/marketplace.json Cursor marketplace manifest — lists every plugin under plugins/
plugins/<name>/
.cursor-plugin/plugin.json Cursor marketplace manifest
.mcp.json server connection (referenced from the manifest)
skills/ workflows the agent can load (SKILL.md each)
rules/ always-on guardrails (.mdc)
assets/ logo and icon
README.md user-facing docs
.cursor-plugin/plugin.json Cursor plugin manifest
mcp.json server connection (Cursor's default MCP config name)
skills/ workflows the agent can load (SKILL.md each)
rules/ always-on guardrails (.mdc)
assets/ logo and icon
README.md user-facing docs
openclaw/<name>/
SKILL.md ClawHub-format skill for OpenClaw agents
server.json entry for the official MCP Registry
llms.txt discovery index for LLM crawlers
scripts/validate.mjs structure checks (run in CI)
SKILL.md ClawHub-format skill for OpenClaw agents
server.json entry for the official MCP Registry
llms.txt discovery index for LLM crawlers
scripts/validate.mjs structure checks (run in CI)
```

Cursor reads the root marketplace manifest first and ingests only the plugins it lists, so a new plugin needs an entry there as well as its own directory.

## Contributing

Run `node scripts/validate.mjs` before opening a PR — CI runs the same checks. [CONTRIBUTING.md](./CONTRIBUTING.md) covers scope, conventions, and local testing; AI agents working in this repo should start with [AGENTS.md](./AGENTS.md).
Expand Down
5 changes: 3 additions & 2 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@

## Manifests

- [Cursor plugin manifest](https://github.com/IFTTT/ifttt-plugins/blob/main/plugins/ifttt/.cursor-plugin/plugin.json): Listing metadata for Cursor's marketplace.
- [MCP server config](https://github.com/IFTTT/ifttt-plugins/blob/main/plugins/ifttt/.mcp.json): Plain `mcpServers` block any MCP client can reuse.
- [Cursor marketplace manifest](https://github.com/IFTTT/ifttt-plugins/blob/main/.cursor-plugin/marketplace.json): Root index Cursor ingests first; lists every plugin in this repo with its directory.
- [Cursor plugin manifest](https://github.com/IFTTT/ifttt-plugins/blob/main/plugins/ifttt/.cursor-plugin/plugin.json): Listing metadata and component paths for the `ifttt` plugin.
- [MCP server config](https://github.com/IFTTT/ifttt-plugins/blob/main/plugins/ifttt/mcp.json): Plain `mcpServers` block any MCP client can reuse.
- [MCP Registry entry](https://github.com/IFTTT/ifttt-plugins/blob/main/server.json): `server.json` for the [official MCP Registry](https://registry.modelcontextprotocol.io).

## Agent guidance
Expand Down
2 changes: 1 addition & 1 deletion plugins/ifttt/.cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,5 @@
"logo": "assets/logo.svg",
"skills": "./skills/",
"rules": "./rules/",
"mcpServers": "./.mcp.json"
"mcpServers": "./mcp.json"
}
2 changes: 1 addition & 1 deletion plugins/ifttt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Automate the services you use every day — Gmail, Google Sheets, Slack, Philips

| Component | Purpose |
|---|---|
| MCP server config (`.mcp.json`) | Streamable HTTP connection to `https://ifttt.com/mcp` with OAuth |
| MCP server config (`mcp.json`) | Streamable HTTP connection to `https://ifttt.com/mcp` with OAuth |
| `ifttt-setup` skill | Authentication, IFTTT concepts, and tool overview |
| `ifttt-build-applet` skill | Step-by-step Applet building, editing, and testing workflow |
| `ifttt-lifecycle` rule | Safety rails: confirmation before destructive or side-effectful calls, no guessed identifiers, connection-error handling |
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion plugins/ifttt/rules/ifttt-lifecycle.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ alwaysApply: true
ifttt-lifecycle:

- Never guess service slugs, trigger/action/query identifiers, or Applet slugs. Always discover them via `search_services`, `get_steps`, `my_applets`, or `get_applet` first.
- If the IFTTT MCP server itself is unauthenticated (tools missing, or every call fails with an auth error), do not read or edit config files such as `.mcp.json` and do not retry in a loop. Direct the user to Cursor Settings → Tools & MCP → Connect next to `ifttt`, then wait for their confirmation before retrying.
- If the IFTTT MCP server itself is unauthenticated (tools missing, or every call fails with an auth error), do not read or edit config files such as `mcp.json` and do not retry in a loop. Direct the user to Cursor Settings → Tools & MCP → Connect next to `ifttt`, then wait for their confirmation before retrying.
- `create_applet` enables the Applet immediately. Summarize what the Applet will do (trigger, actions, and any queries or delay) and get the user's confirmation before creating it.
- Confirm with the user before calling `remove_applet` (permanent) or `run_action` (performs the action for real — messages get sent, devices get switched).
- Copy `step_template` from discovery results verbatim when building Applet configs; only fill in `account_id` and field values. Do not invent field names.
Expand Down
2 changes: 1 addition & 1 deletion plugins/ifttt/skills/ifttt-setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ description: Set up and authenticate the IFTTT MCP connection, understand IFTTT

There are two separate layers of authentication. Do not confuse them.

**Layer 1 — connecting the MCP server itself (Cursor-managed OAuth).** No IFTTT tool performs this login, and it cannot be fixed by editing `.mcp.json` or other config files — never attempt that. If IFTTT tools are unavailable or every call fails with an authentication error:
**Layer 1 — connecting the MCP server itself (Cursor-managed OAuth).** No IFTTT tool performs this login, and it cannot be fixed by editing `mcp.json` or other config files — never attempt that. If IFTTT tools are unavailable or every call fails with an authentication error:

1. If Cursor offers an authentication action for the `ifttt` server, trigger it once. The browser should open IFTTT's authorization page, where the user signs in (or creates a free account) and approves access.
2. If the browser does not open or auth still fails, hand off to the user: "Open **Cursor Settings → Tools & MCP**, find the `ifttt` server, and click **Connect**. Sign in at ifttt.com and approve access." Then stop and wait for the user to confirm before retrying — do not investigate config files or retry in a loop.
Expand Down
106 changes: 106 additions & 0 deletions scripts/validate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,32 @@ const warnings = [];

const pluginNamePattern = /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/;

// Top-level fields accepted by Cursor's plugin manifest schema (cursor/plugins, schemas/plugin.schema.json).
// That schema sets additionalProperties: false, so any other field fails marketplace ingestion.
const cursorPluginManifestFields = new Set([
"name",
"displayName",
"description",
"version",
"minClientVersions",
"author",
"publisher",
"homepage",
"repository",
"license",
"logo",
"keywords",
"category",
"tags",
"commands",
"agents",
"skills",
"rules",
"hooks",
"variables",
"mcpServers",
]);

function addError(message) {
errors.push(message);
}
Expand Down Expand Up @@ -224,6 +250,12 @@ async function validatePlugin(pluginDir, dirName) {
addError(`${dirName}: plugin.json name ("${manifest.name}") must match the plugin directory name.`);
}

for (const field of Object.keys(manifest)) {
if (!cursorPluginManifestFields.has(field)) {
addError(`${dirName}: plugin.json field "${field}" is not in Cursor's plugin manifest schema, which rejects unknown fields.`);
}
}

for (const field of ["displayName", "version", "description", "license"]) {
if (typeof manifest[field] !== "string" || manifest[field].length === 0) {
addError(`${dirName}: plugin.json is missing required field "${field}".`);
Expand Down Expand Up @@ -253,6 +285,79 @@ async function validatePlugin(pluginDir, dirName) {
}
}

// Cursor's ingester reads .cursor-plugin/marketplace.json at the repo root and only sees plugins listed there,
// resolving each "source" to a directory that must contain .cursor-plugin/plugin.json.
async function validateMarketplaceManifest(pluginDirNames) {
const marketplacePath = path.join(repoRoot, ".cursor-plugin", "marketplace.json");
const manifest = await readJsonFile(marketplacePath, "Cursor marketplace manifest");
if (!manifest) {
return;
}

if (typeof manifest.name !== "string" || manifest.name.length === 0) {
addError('marketplace.json is missing required field "name".');
}

if (!manifest.owner || typeof manifest.owner.name !== "string" || manifest.owner.name.length === 0) {
addError('marketplace.json "owner.name" is required.');
}

if (!Array.isArray(manifest.plugins) || manifest.plugins.length === 0) {
addError('marketplace.json "plugins" must be a non-empty array.');
return;
}

const listedNames = new Set();
const listedDirs = new Set();

for (const [index, entry] of manifest.plugins.entries()) {
const label = `marketplace.json plugins[${index}]`;
if (!entry || typeof entry !== "object") {
addError(`${label} must be an object with "name" and "source".`);
continue;
}

if (typeof entry.name !== "string" || !pluginNamePattern.test(entry.name)) {
addError(`${label}: "name" must be lowercase and use only alphanumerics, hyphens, and periods.`);
} else if (listedNames.has(entry.name)) {
addError(`${label}: duplicate plugin name "${entry.name}"; marketplace plugin names must be unique.`);
} else {
listedNames.add(entry.name);
}

if (
typeof entry.source !== "string" ||
entry.source.startsWith("http://") ||
entry.source.startsWith("https://") ||
!isSafeRelativePath(entry.source)
) {
addError(`${label}: "source" must be a relative path to a plugin directory in this repo (got ${JSON.stringify(entry.source)}).`);
continue;
}

const sourceDir = path.resolve(repoRoot, entry.source);
const sourceManifestPath = path.join(sourceDir, ".cursor-plugin", "plugin.json");
if (!(await pathExists(sourceManifestPath))) {
addError(`${label}: "source" ("${entry.source}") does not contain .cursor-plugin/plugin.json.`);
continue;
}
listedDirs.add(path.relative(repoRoot, sourceDir).split(path.sep).join("/"));

const sourceManifest = await readJsonFile(sourceManifestPath, `${label} target plugin manifest`);
if (sourceManifest && sourceManifest.name !== entry.name) {
addError(
`${label}: name ("${entry.name}") must match the plugin.json name ("${sourceManifest.name}") in "${entry.source}".`,
);
}
}

for (const dirName of pluginDirNames) {
if (!listedDirs.has(`plugins/${dirName}`)) {
addError(`${dirName}: plugins/${dirName} is not listed in .cursor-plugin/marketplace.json, so Cursor will not ingest it.`);
}
}
}

const openclawSkillNamePattern = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;

// Full SemVer 2.0.0 pattern from semver.org, kept inline so the validator stays dependency-free.
Expand Down Expand Up @@ -360,6 +465,7 @@ async function main() {
await validatePlugin(path.join(pluginsRoot, entry.name), entry.name);
}

await validateMarketplaceManifest(pluginDirs.map((entry) => entry.name));
await validateOpenclawSkills();
await validateRegistryManifest();

Expand Down
Loading