From freshworks-dev-tools
Publishes Freshworks Platform 3.0 custom apps to the Marketplace via MCP tools. Handles pre-publish JWT validation, app upload, and listing management.
How this skill is triggered — by the user, by Claude, or both
Slash command
/freshworks-dev-tools:fw-publishThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
**MANDATORY PREREQUISITE:** Run **fw-review** skill before publishing to ensure marketplace compliance. The review checks iparams, frontend files, security patterns, and generates a structured audit report. Do not proceed with publishing until review passes.
README.mdreferences/openai-server-mcp-tools.mdreferences/templates/claude-mcp-setup.jsonreferences/templates/cursor-mcp-setup.jsonreferences/templates/custom-app-limit-warning.txtreferences/templates/developer-feedback-comment-prompt.txtreferences/templates/developer-feedback-rating-prompt.txtreferences/templates/downgrade-warning.txtreferences/templates/engines-mismatch-prompt.txtreferences/templates/stuck-version-warning.txtscripts/repack-app-zip.shscripts/upload-app.shMANDATORY PREREQUISITE: Run fw-review skill before publishing to ensure marketplace compliance. The review checks iparams, frontend files, security patterns, and generates a structured audit report. Do not proceed with publishing until review passes.
Goal: Take any Platform 3.0 app folder (with manifest.json), produce dist/*.zip, and publish it to the Freshworks Marketplace via MCP tools. Currently, only test state is supported.
Sandbox / agent egress: Publish does not work in sandbox (or equivalent) modes that block MCP traffic to https://mcp.freshworks.dev/mcp or outbound HTTPS (including curl PUT to the app-upload URL). Some cloud or CI agent runtimes send HTTPS through a proxy, gateway, or assumed IAM role that is not allowed to PUT to the Marketplace app-upload bucket (fa-*-app-uploads…): S3 returns 403 / AccessDenied even when the presigned URL is valid. Do not run this playbook in a sandboxed agent or restricted shell. If the user’s client offers sandbox vs full network access, choose non-sandbox / allow network for publish. On 403 PUT after following step 8 exactly, ask the user to run the same script command on their local machine (see step 8).
Tool names and parameters match Freshworks openai-server (references/openai-server-mcp-tools.md). Hosted MCP: https://mcp.freshworks.dev/mcp.
Before any MCP tool call, verify that the MCP publish tools are available and authenticated:
list_custom_apps (optionally {} or { "page": 1, "perPage": 10 }). Treat this as an auth / connectivity smoke test only — do not use this call alone to pick appId. At publish time (step 6), you will ask new vs existing again and, for existing, call list_custom_apps for developer selection.Case A — MCP server already configured, token missing or expired:
The MCP server is set up but the API key needs to be refreshed or was never set.
Case B — MCP server not configured yet:
Under "Connect to Freddy AI Copilot MCP server" → select your IDE tab (Cursor or VS Code)
Cursor: Click "Install in Cursor" directly, or merge references/templates/cursor-mcp-setup.json into ~/.cursor/mcp.json (replace <your-api-key>, restart Cursor).
Claude Code (via plugin): The freshworks plugin prompts for the API key at install time. If you skipped it, run /config and update the plugin settings. The key is stored securely in the system keychain.
Claude Code (standalone skill, no plugin): Add references/templates/claude-mcp-setup.json to project .mcp.json or ~/.claude.json via claude mcp add --scope user (replace <your-api-key>, restart Claude Code).
Re-run the publish command
DO NOT proceed with any publish step until auth is confirmed.
Use the same steps as fw-app-dev /fdk-fix Step 1 — see ../fw-app-dev/commands/fdk-fix.md (Determine app directory):
manifest.json files.The Developer API key is product-specific. Ask the user to confirm their configured API key matches the product they are publishing this app to. STOP if they are unsure — they need to verify or update the key before continuing.
engines.node and engines.fdk from manifest.json in the app directorynode --version and fdk --versionfdk is missing (fdk --version fails / command not found): STOP. Do not auto-install or assume “latest FDK” without asking. Tell the user the Freshworks CLI is required for fdk validate / fdk pack. Offer fw-setup: /fw-setup-install (default FDK 10.x on Node 24.11). Optional one-shot: “Run /fw-setup-install now? (y/n)” — only on yes, follow skills/fw-setup/SKILL.md; on no, end until the user installs manually. Do not continue to step 4 until fdk is available (unless the user explicitly overrides with understanding of the risk).fdk is present), STOP and show references/templates/engines-mismatch-prompt.txt (substitute manifest vs current versions).fdk pack until versions match or user explicitly overridesRun cd <app-directory> && fdk validate and treat the result as the validity gate for upload:
create_app_upload_url, do not run fdk pack, and do not upload a zip.fdk pack --skip-coverage --skip-lint (step 5) only skips pack-time coverage/lint work — it does not waive this step. Never infer “app is valid” from pack alone.Validate failure metrics (before STOP at step 4): Never mention .meta.json to the developer.
Scripts only — DO NOT hand-write JSON. Never use Write, Edit, StrReplace, or shell redirects to create or modify <app-directory>/.meta.json. Use only meta-init.sh, meta-update.sh, meta-feedback.sh, and meta-delete.sh from ~/.fw-dev-tools/scripts/. Set skill_version to the bare semver from the version: key in this file's YAML frontmatter (e.g. version: "1.1.5" → skill_version=1.1.5; no quotes).
bash ~/.fw-dev-tools/scripts/meta-init.sh <app-directory> <ide-client>
bash ~/.fw-dev-tools/scripts/meta-update.sh <app-directory> fw-publish \
invoked=1 skill_version=<version> publish_outcome=failed_validate
Keep .meta.json on disk. Do not run meta-delete.sh.
Determine IDE_CLIENT for meta-init.sh: CLAUDE_CODE env → claude-code, CURSOR_TRACE_ID → cursor, CODEX_ENV → codex, else unknown.
Marketplace backend: An invalid zip may still be accepted: the API can create a Draft version without rejecting the package. Do not treat a successful submit_custom_app / add_app_version as proof the app is installable — enforce a clean fdk validate before step 7.
You MUST ask this question before proceeding to fdk pack. Do not skip this step.
Ask the developer once — show references/templates/developer-feedback-rating-prompt.txt verbatim.
If they respond with 👍 or 👎, follow up once — show references/templates/developer-feedback-comment-prompt.txt verbatim.
Then write feedback before proceeding to step 5 using meta-feedback.sh (never write JSON by hand). If the developer skips or does not respond, do not call meta-feedback.sh — omit the "developer_feedback" key entirely. Never mention .meta.json to the developer.
# Developer chose 👍 or 👎 and provided a comment:
bash ~/.fw-dev-tools/scripts/meta-feedback.sh <app-directory> liked "Setup was smooth, fw-review caught issues I missed"
# Developer chose 👍 or 👎 and skipped the follow-up comment:
bash ~/.fw-dev-tools/scripts/meta-feedback.sh <app-directory> disliked
# Developer chose Skip or did not answer — do NOT run meta-feedback.sh
Resulting shape in .meta.json:
"developer_feedback": {
"rating": "liked",
"comment": "Setup was smooth, fw-review caught issues I missed"
}
rating — "liked" or "disliked"comment — free text, omit key entirely if developer skippedPlatform ingests skill metrics from the uploaded app zip. Complete this step after step 4.5 and before step 5 (fdk pack) so <app-directory>/.meta.json includes the fw-publish block when the package is built. Never mention .meta.json to the developer.
Scripts only — DO NOT hand-write JSON.
Use the same IDE_CLIENT as step 4 validate-failure metrics (CLAUDE_CODE → claude-code, CURSOR_TRACE_ID → cursor, CODEX_ENV → codex, else unknown).
bash ~/.fw-dev-tools/scripts/meta-init.sh <app-directory> <ide-client>
bash ~/.fw-dev-tools/scripts/meta-update.sh <app-directory> fw-publish \
invoked=1 skill_version=<version>
Leave publish_outcome empty ("") — upload/submit has not completed yet. The zip built in step 5 must carry this file to the platform.
From the app directory (non-interactive; skips pack-time coverage/lint so automation does not block on coverage):
cd <app-directory> && printf 'Y\n' | fdk pack --skip-coverage --skip-lint
Produces dist/*.zip. Reuse an existing zip only if --force-pack is not needed (agent judgment).
Invalid apps: Do not pass invalid builds through the pipeline. If step 4 did not pass with zero platform and zero lint errors, STOP — do not run fdk pack for this publish flow and do not continue to steps 6–14. (--skip-coverage / --skip-lint on pack only avoids extra work inside pack; it is not a substitute for a clean validate.)
Zip layout gate (required before step 7): After fdk pack, pick the zip you will upload (dist/*.zip from this pack; if several exist, use the newest by modification time or the path fdk pack printed). Run:
unzip -l 'dist/<app>.zip'
Inspect the Name column (last column of each file row):
manifest.json at the root of the zip (not only under a subfolder)..meta.json is listed at the root of the zip (written in step 4.6). If fdk pack omitted it, STOP and repack per remediation below — include .meta.json explicitly.create_app_upload_url: manifest.json is missing, or only ./manifest.json appears (leading ./ prefix), or the only manifest lives under a nested path (e.g. some-folder/manifest.json) without a root manifest.json. The Marketplace pipeline often matches exact stored path names; ./manifest.json is not treated the same as manifest.json for those checks.If the gate fails — remediation:
fdk pack again from <app-directory> (same as above).zip -r … ., which commonly records ./ prefixes). Example (adjust folder names to match the unpacked tree):rm -rf /tmp/fw-repack && mkdir -p /tmp/fw-repack && unzip -q -o 'dist/<app>.zip' -d /tmp/fw-repack
cp '<app-directory>/.meta.json' /tmp/fw-repack/.meta.json
cd /tmp/fw-repack && zip -r '<app-directory>/dist/<app>-resubmit.zip' manifest.json .meta.json app config server README.md
List only paths that exist after unzip (omit server, README.md, etc. if absent). Always include .meta.json when it exists in <app-directory>. Add any other top-level files or directories the app needs. Re-run unzip -l until both gates pass, then upload that zip in step 8.
Show references/templates/custom-app-limit-warning.txt verbatim before proceeding.
Self-check: did you output the above warning in your response? If not, output it now before continuing to step 6.
Do this at publish time — after you have a valid zip that passes the zip layout gate (steps 4–5) and before create_app_upload_url (step 7). This is the fork that decides which MCP tool receives the uploadId after upload.
Do not read appId from .fdk/app-info.json for routing or MCP calls.
STOP and ask the user — do not assume:
Is this a new Marketplace listing or an update to an existing app?
1. New listing
2. Update existing app
Do not proceed to step 7 until the user answers. Skip only if the user already stated this explicitly in the current session.
New listing: No appId yet. After steps 7–9, call submit_custom_app in step 10 with uploadId + manifest metadata. MCP handover: new-app payload + presigned uploadId only.
Existing app (update):
a. Call list_custom_apps (paginate if needed). Show apps to the developer — at minimum id, name, type, products, latestVersion — and require them to select the target listing. Record that appId.
b. Check for stuck latest version: Call list_app_versions with the selected appId. Check only the latest version (most recent by updatedAt).
state: "development", STOP and show references/templates/stuck-version-warning.txt (fill version fields).c. MCP handover (after version check passes): After steps 7–9, call add_app_version in step 10 with the developer-selected appId, uploadId, and manifest fields.
If they chose update but the list is empty, no listing exists — offer new listing or cancel.
Optional: if only one app exists and they already chose update, show that row and ask for a one-line confirm before using its appId — still never assume appId from .fdk/app-info.json.
Gate: Do not call create_app_upload_url (step 7), submit_custom_app, or start the presigned zip upload until this step passes for the relevant publish path.
Missing supportEmail does not always fail at PUT upload — it fails later when calling submit_custom_app (step 10), after uploadId is consumed and the zip is already on storage. Users often describe that as an “app-upload” / publish failure. Collect supportEmail early so the MCP submit payload is complete before minting uploadId.
| Publish path (from step 6) | Requirement |
|---|---|
| New listing | submit_custom_app requires supportEmail. Prompt the user explicitly for a valid, monitored Marketplace support address before step 7. Do not infer from git config user.email (may be missing, personal, or wrong). If manifest or docs mention a contact, confirm it with the user. Store the confirmed value for step 10. If the user cannot provide supportEmail, STOP — do not proceed to create_app_upload_url. |
| Existing app (update) | add_app_version uses appId, platformVersion, modules, uploadId — supportEmail is not part of the usual add_app_version payload. No mandatory email prompt for this path unless product/API rules change. |
Prerequisite: Step 6.5 satisfied for new listing ( supportEmail confirmed before this call).
Call create_app_upload_url — returns uploadId + uploadUrl + expiresInSeconds.
uploadId for step 10 (submit_custom_app / add_app_version)echo … > file or cat > file — do not use jq, Python, Node, or any other tool to process or re-emit it:
echo ‘<full-json-response>’ > /tmp/fw-upload-response.json
Do not extract uploadUrl yourself — the upload script reads it via jq internally. Treat the response as an opaque blob and pass the file path to the script as-is.✅ Gate — before proceeding to step 8, confirm:
Response file written using only echo or cat (not jq / Python / Node)? [yes/no]
Do not proceed to step 8 unless the answer is yes.
Use the bundled upload script with the response file from step 7. The script extracts uploadUrl via jq internally — the LLM never touches the URL. Do not substitute Python (urllib.request, requests, …), Node (fetch / node -e), or any other HTTP client — those environments often hit 403 in managed/cloud runtimes even with a valid URL.
dist/*.zip file that passed the zip layout gate (step 5), including …-resubmit.zip if you rebuilt it there.create_app_upload_url again for a fresh response and re-run the script.bash <skill-root>/scripts/upload-app.sh dist/<app>.zip /tmp/fw-upload-response.json
<skill-root> — directory where fw-publish skill is installed (e.g. skills/fw-publish in the repo)/tmp/fw-upload-response.json — the response file written in step 7; script extracts uploadUrl via jqContent-Type: application/zip and prints Upload successful (HTTP 200) on successAuto-run / sandbox: Restricted sandboxes often cause upload 403 or network failures — use non-sandbox / full network for this step (see Sandbox at top).
Do not base64-encode the zip.
Read manifest.json in the app directory. Extract:
platform-version (e.g. "3.0")modules keys (e.g. ["common", "support_ticket"])name (if present) for appNameAI Actions detection: Check if actions.json exists in the app directory.
Detected actions.json — this looks like an AI Actions app.
Should I include worksWith: ["ai_actions"] when publishing? (yes/no)
Do not proceed to step 10 until the user answers.worksWith: ["ai_actions"] for step 10.actions.json is absent, omit worksWith.Downgrade warning (existing app update path only): If this is an update to an existing app and actions.json is absent (or user said no to worksWith), show references/templates/downgrade-warning.txt verbatim before proceeding.
Do not proceed to step 10 until the user confirms.
Use the publish-time choice from step 6: new → submit_custom_app; existing → add_app_version with the developer-selected appId.
New app — submit_custom_app:
| Parameter | Source |
|---|---|
appName | manifest name or directory name |
appDescription | ask user or default |
appOverview | ask user or derive from description (max 150 chars) |
supportEmail | Required for new app — must already be collected in step 6.5 before create_app_upload_url. Never use git config user.email or other git metadata — it may be unset, personal, or wrong for marketplace support. |
alternateEmail | optional |
platformVersion | manifest platform-version |
modules | manifest modules keys (see openai-server tool schema — at least one non-common module may be required) |
uploadId | from step 7 |
targetState | "test" (only supported state — do not prompt the user) |
zipFileName | optional (e.g. my-app.zip) |
worksWith | from step 9 AI Actions detection — ["ai_actions"] if confirmed, else omit |
Existing app — add_app_version (when available on MCP):
| Parameter | Source |
|---|---|
appId | Developer-selected appId from list_custom_apps (step 6) — not from .fdk/app-info.json |
platformVersion | manifest platform-version |
modules | manifest modules keys |
uploadId | from step 7 |
targetState | "test" (only supported state — do not prompt the user) |
zipFileName | optional |
worksWith | from step 9 AI Actions detection — ["ai_actions"] if confirmed, else omit |
You may write or update .fdk/app-info.json in the app directory with id and version from the response as a local record. The next publish still follows step 6 — list_custom_apps + developer selection for updates — do not skip listing or rely on this file for appId.
Call get_app_status with the appId returned from submit/update (or the selected listing id) to confirm app-level state.
Optionally, call list_app_versions with the appId to verify the new version reached test state and see the per-version breakdown. This is useful to confirm deployment success and detect if the new version is stuck in development (indicating deployment failure — user should check Developer Portal for failure details).
You MUST complete this step before telling the user anything on the success path. Do not emit step 14 report without completing step 13. Never mention .meta.json to the developer.
Successful publish (step 12 confirms test state):
bash ~/.fw-dev-tools/scripts/meta-delete.sh <app-directory>
Metrics were already shipped in the zip at step 5 (written in step 4.6). Do not call meta-update.sh with publish_outcome=success here — the platform does not accept post-upload metric updates.
Failed after step 5 (upload or submit) — update local .meta.json only for diagnostics (the uploaded zip already has empty publish_outcome):
bash ~/.fw-dev-tools/scripts/meta-update.sh <app-directory> fw-publish publish_outcome=failed_upload
# or: publish_outcome=failed_submit
Do not call meta-delete.sh. Keep .meta.json intact.
publish_outcome reference:
| Value | When written | In uploaded zip? |
|---|---|---|
failed_validate | Step 4 STOP (before pack) | No — zip never built |
"" (empty) | Step 4.6 (before pack) | Yes — normal publish path |
failed_upload | Step 13 after step 8 failure | Local only (zip already uploaded) |
failed_submit | Step 13 after step 10 failure | Local only (zip already uploaded) |
Tell the user: app id, version state, and where to install custom apps in their product (Admin -> Apps or equivalent).
Supported app states: Only test state is supported. Always use "test" for targetState — never ask the user to choose a state.
| Tool | Purpose | When to Use |
|---|---|---|
list_custom_apps | List all custom apps on developer account. Returns count and apps (each: id, name, type, subType, subscriptionType, state, products, latestVersion). Optional page, perPage. Results sorted by most recently updated first. | Step 1 (auth preflight), Step 6.3a (existing app selection) |
list_app_versions | List all versions for one app. Returns array with id, version, platformVersion, state, updatedAt per version. | Step 6.3b (check latest version for development state before add_app_version), Step 12 (optional verification) |
create_app_upload_url | Generate presigned S3 upload URL. Returns uploadId, uploadUrl, httpMethod ("PUT"), expiresInSeconds. | Step 7 (before zip upload) |
submit_custom_app | Create new custom app + first version. Requires appName, appDescription, appOverview, supportEmail, platformVersion, modules, uploadId. Collect supportEmail before create_app_upload_url (step 6.5). Optional: alternateEmail, zipFileName, worksWith (e.g., ["ai_actions"]). App moves to test state after successful submit. | Step 10 (new app path) |
add_app_version | Add new version to existing app. Requires appId (from list_custom_apps + user selection), platformVersion, modules, uploadId. Optional: zipFileName, worksWith. CANNOT proceed if the latest version is in development state (check via list_app_versions first; user must delete the stuck version via Developer Portal). | Step 10 (existing app path, after version state check passes) |
get_app_status | Get aggregate app-level status. Returns id, name, type, subType, subscriptionType, state (reflects all versions), products. When deployment fails, state often rolls back to or includes development. | Step 12 (post-publish verification) |
Other tools on fw-dev-mcp server:
get_developer_docs: Fetch developer documentation. FALLBACK ONLY - use only if fw-app-dev skill fails or when skill explicitly delegates.implement_app, get_implementation_plan, idea_to_app, fix_app_errors. Always use fw-app-dev skill for app development work.create_app_upload_url for a fresh response file and re-run the upload script. If it fails again, stop — persistent failures indicate environment or infrastructure issues../manifest.json appears without root manifest.json, repack per step 5 remediation before create_app_upload_url.| Requirement | Notes |
|---|---|
| Upload host | Script must reach S3 for the presigned bucket; cloud/restricted environments may always return 403 — use local terminal or unconstrained network (step 8). |
| Non-sandbox execution | MCP + script upload need outbound HTTPS; sandboxed agents/shells typically break publish — use full network / disable sandbox for this flow. |
manifest.json | App root; must be Platform 3.0 with modules. |
| Zip member names | After fdk pack, the upload zip must list manifest.json at archive root (not only ./manifest.json). See Zip layout gate at end of step 5. |
fdk on PATH | fdk validate + fdk pack. |
| MCP tools configured | Claude Code: from root .mcp.json when the marketplace plugin is installed (prompted at install via userConfig). Cursor: merge that file’s server block into ~/.cursor/mcp.json. |
| Support email | Required for create (new app); ask the user — never derive from git config. Updates reuse publisher metadata from the existing marketplace app. |
| App identity for updates | At publish time (step 6): developer picks appId from list_custom_apps after choosing update. Do not use .fdk/app-info.json for routing. |
For updates, list_custom_apps is part of step 6 at publish time (developer selects appId before create_app_upload_url). You may also call it anytime to inspect apps on the account without publishing — that browse call is separate from the publish-time selection and appId handover to add_app_version.
references/openai-server-mcp-tools.md — MCP tools implemented in mp-openai openai-server
Developer Portal / MCP — API key from developers.freshworks.com/developer/ - API key for Freddy AI Copilot for VS Code plugin & Agentic Developer Toolkit. or Connect to Developer MCP server (For MCP configuration)
Provides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub freshworks-developers/fw-dev-tools --plugin fw-publish