From 1246c20de468edb4d90aaa3a28607c0bf9a45c48 Mon Sep 17 00:00:00 2001 From: Siddharth Sathyam Date: Mon, 21 Sep 2026 14:08:06 -0500 Subject: [PATCH] Add the root Cursor marketplace manifest and canonical mcp.json [sc-143999] Cursor's marketplace reviewer could not ingest the plugin: this repo uses the multi-plugin layout (plugins//.cursor-plugin/plugin.json) but had no .cursor-plugin/marketplace.json at the root, which is the file Cursor's ingester reads to find plugins in that layout. - Add .cursor-plugin/marketplace.json listing the ifttt plugin with source "plugins/ifttt", shaped per cursor/plugins' marketplace.schema.json. - Rename plugins/ifttt/.mcp.json to mcp.json, the name Cursor discovers by default and the one the Agent Plugins layout expects; repoint the manifest's mcpServers field and every doc reference. Keep "type": "http", the value used by every ingested Cursor plugin. - Keep category, tags, and displayName: Cursor's plugin.schema.json lists them even though the prose docs' field table omits them. - Teach scripts/validate.mjs to require the marketplace manifest, check that plugin names are unique and match each target plugin.json, that each source resolves to a directory with .cursor-plugin/plugin.json, that every plugins// is listed, and that plugin.json uses no fields outside Cursor's schema (which sets additionalProperties: false). - Widen the CI JSON-parse glob and update README, AGENTS.md, CONTRIBUTING, llms.txt, and the plugin README to describe the real layout. An Agent Plugins root plugin.json was deliberately not added: Cursor's docs say that format loads only skills and MCP servers (no rules), document no precedence when both manifests exist, and its mcp.json schema requires "type": "streamable-http" plus a $schema key that no ingested Cursor plugin uses, so a single mcp.json cannot satisfy both. --- .cursor-plugin/marketplace.json | 17 ++++ .github/workflows/validate.yml | 2 +- AGENTS.md | 7 +- CONTRIBUTING.md | 8 +- README.md | 23 +++-- llms.txt | 5 +- plugins/ifttt/.cursor-plugin/plugin.json | 2 +- plugins/ifttt/README.md | 2 +- plugins/ifttt/{.mcp.json => mcp.json} | 0 plugins/ifttt/rules/ifttt-lifecycle.mdc | 2 +- plugins/ifttt/skills/ifttt-setup/SKILL.md | 2 +- scripts/validate.mjs | 106 ++++++++++++++++++++++ 12 files changed, 153 insertions(+), 23 deletions(-) create mode 100644 .cursor-plugin/marketplace.json rename plugins/ifttt/{.mcp.json => mcp.json} (100%) diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json new file mode 100644 index 0000000..d1bd86a --- /dev/null +++ b/.cursor-plugin/marketplace.json @@ -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." + } + ] +} diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 2012f01..f82982a 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -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 diff --git a/AGENTS.md b/AGENTS.md index 4674626..ada885a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 @@ -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) | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 19c7c18..444a515 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,8 +7,10 @@ The MCP server itself is not developed here. Problems with tool behavior, the se ## Adding a plugin 1. Create `plugins//` (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/`), 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 @@ -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//` 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 diff --git a/README.md b/README.md index bad2eed..2cf24e7 100644 --- a/README.md +++ b/README.md @@ -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// - .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// - 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). diff --git a/llms.txt b/llms.txt index e8dcfda..f856698 100644 --- a/llms.txt +++ b/llms.txt @@ -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 diff --git a/plugins/ifttt/.cursor-plugin/plugin.json b/plugins/ifttt/.cursor-plugin/plugin.json index d68d90c..1b89893 100644 --- a/plugins/ifttt/.cursor-plugin/plugin.json +++ b/plugins/ifttt/.cursor-plugin/plugin.json @@ -36,5 +36,5 @@ "logo": "assets/logo.svg", "skills": "./skills/", "rules": "./rules/", - "mcpServers": "./.mcp.json" + "mcpServers": "./mcp.json" } diff --git a/plugins/ifttt/README.md b/plugins/ifttt/README.md index f469dd2..e787e8f 100644 --- a/plugins/ifttt/README.md +++ b/plugins/ifttt/README.md @@ -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 | diff --git a/plugins/ifttt/.mcp.json b/plugins/ifttt/mcp.json similarity index 100% rename from plugins/ifttt/.mcp.json rename to plugins/ifttt/mcp.json diff --git a/plugins/ifttt/rules/ifttt-lifecycle.mdc b/plugins/ifttt/rules/ifttt-lifecycle.mdc index 808f6da..65b4dde 100644 --- a/plugins/ifttt/rules/ifttt-lifecycle.mdc +++ b/plugins/ifttt/rules/ifttt-lifecycle.mdc @@ -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. diff --git a/plugins/ifttt/skills/ifttt-setup/SKILL.md b/plugins/ifttt/skills/ifttt-setup/SKILL.md index 9edbe2f..188f863 100644 --- a/plugins/ifttt/skills/ifttt-setup/SKILL.md +++ b/plugins/ifttt/skills/ifttt-setup/SKILL.md @@ -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. diff --git a/scripts/validate.mjs b/scripts/validate.mjs index 63481e8..f1ec111 100644 --- a/scripts/validate.mjs +++ b/scripts/validate.mjs @@ -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); } @@ -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}".`); @@ -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. @@ -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();