Skip to content
Open
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
3 changes: 2 additions & 1 deletion .agents/plugins/marketplace.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "voiceflow-plugins", "interface": { "displayName": "Voiceflow" },
"plugins": [{ "name": "voiceflow", "source": { "source": "local", "path": "./voiceflow" },
"policy": { "installation": "AVAILABLE", "authentication": "ON_INSTALL" } }]
"policy": { "installation": "AVAILABLE", "authentication": "ON_INSTALL" },
"category": "Developer Tools" }]
}
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ from `https://mcp.voiceflow.com/mcp?version=2` to
provisional until the docs server ships the option.
- Send only the package, skill, and stage tag in the docs `why` field, with no
task, symptom, or setup-state sentence.
- Add a plugin README that explains setup, the services the plugin contacts,
and what each one receives. Anthropic's directory shows it as the listing.
- Add directory listing metadata: display name, links, keywords, the
`Developer Tools` category, starter prompts, and `voiceflow-setup` as the
onboarding skill. The listing icon is still missing.
- Add `skills.sh.json` so `npx skills add voiceflow/agent-plugins` finds every
skill under `voiceflow/skills`.
- Add MCP Registry entries for the account and docs servers. They are not
published.
- Refuse a `vf_*` name that v3 does not serve, and every older
`voiceflow_<group>` name, in the skill reference checks.
- Add the Optimize playbooks skill: condense long instructions, or move a job
Expand Down
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ python3 scripts/build.py
| `tests/fixtures/voiceflow-surface.json` | MCP and CLI names the skill checks accept; not packaged |
| `evals/` | Behavioral cases, protocols, and sanitized evidence |
| `docs/` | Compatibility, release, provenance, and measurement guidance |
| `skills.sh.json` | Tells the skills CLI where the skills live; not packaged |
| `mcp-registry/` | MCP Registry entries for the two remote servers; not packaged or published |
| `dist/` | Generated output; do not commit it |
| `package-files.txt` | Reviewed list of files allowed into published packages |
| `AGENTS.md` and `CLAUDE.md` | Shared contributor rules and Claude Code entrypoint |
Expand Down
6 changes: 6 additions & 0 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,5 +155,11 @@ plugin name `voiceflow`, marketplace name `voiceflow-plugins`, and skill names
stable. Do not recreate the repository at its previous location: that removes
GitHub's redirect. Marketplace submission is independent of repository publication.

Directory listings read the plugin manifests and `voiceflow/README.md`.
`scripts/validate.py` checks that the manifests share their links, keywords,
category, and onboarding skill. Anthropic's portal may take the listing icon
only from the first save or submission, so add the official icon first.
Submitting to a directory is an owner decision, like publishing a release.

Package docs connections turn off Knowledge MCP logging, as explained in
[measurement](measurement.md); releases do not add a separate analytics service.
20 changes: 20 additions & 0 deletions mcp-registry/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# MCP Registry entries

These files describe Voiceflow's two remote MCP servers for the official
[MCP Registry](https://registry.modelcontextprotocol.io). They are not part of
the plugin package, and nothing in this repository publishes them.

| Folder | Server | URL |
| --- | --- | --- |
| `voiceflow/` | Voiceflow account MCP, OAuth | `https://mcp.voiceflow.com/mcp` |
| `knowledge/` | Voiceflow Knowledge docs MCP, no sign-in | The public docs MCP URL, without the package's `logging=off` option |

The docs entry has no `logging=off` option. People who add the server from the
registry are treated like people who add it by hand, so the docs server can log
their requests.

Publishing needs an owner decision on the server names, because a published
name is hard to change. The `com.voiceflow/` namespace also needs DNS or HTTP
verification of `voiceflow.com`. See the registry's
[authentication guide](https://github.com/modelcontextprotocol/registry/blob/main/docs/modelcontextprotocol-io/authentication.mdx).
Each published version needs a new `version` value.
14 changes: 14 additions & 0 deletions mcp-registry/knowledge/server.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "com.voiceflow/knowledge",
"title": "Voiceflow Knowledge",
"description": "Answers Voiceflow product, API, and CLI questions from the public docs. No sign-in.",
"version": "2.1.2",
"websiteUrl": "https://www.voiceflow.com/docs/mcp/knowledge",
"remotes": [
{
"type": "streamable-http",
"url": "https://www.voiceflow.com/docs/mcp"
}
]
}
14 changes: 14 additions & 0 deletions mcp-registry/voiceflow/server.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "com.voiceflow/voiceflow",
"title": "Voiceflow",
"description": "Build, test, and manage Voiceflow agents in your workspace. Sign in with OAuth.",
"version": "3.0.0",
"websiteUrl": "https://www.voiceflow.com/docs",
"remotes": [
{
"type": "streamable-http",
"url": "https://mcp.voiceflow.com/mcp"
}
]
}
1 change: 1 addition & 0 deletions package-files.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ voiceflow/.codex-plugin/plugin.json
voiceflow/.cursor-plugin/plugin.json
voiceflow/.mcp.json
voiceflow/LICENSE
voiceflow/README.md
voiceflow/mcp.json
voiceflow/plugin.json
voiceflow/skills/voiceflow-build/LICENSE
Expand Down
76 changes: 75 additions & 1 deletion scripts/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@
DOCS_MCP_URL = "https://www.voiceflow.com/docs/mcp?logging=off"
DOCS_MCP_ENDPOINT = re.compile(r"https://www\.voiceflow\.com/docs/mcp(?![\w/-])(?:\?[\w=&%-]+)?")
DOCS_URL_SCOPE = ("voiceflow/**/*", "*.md", "docs/*.md", "evals/*.md", "scripts/*.py", "tests/*.py")
ACCOUNT_MCP_URL = "https://mcp.voiceflow.com/mcp"
REGISTRY_SCHEMA = "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json"
SKILLS_SH_SCHEMA = "https://skills.sh/schemas/skills.sh.schema.json"
KEYWORD = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*")
SEMVER = re.compile(r"\d+\.\d+\.\d+")
# Limits from developers.openai.com/plugins/deploy/submission (listing metadata).
INTERFACE_LIMITS = {"displayName": 30, "shortDescription": 30, "longDescription": 4000, "developerName": 80,
"category": 80, "websiteURL": 1024, "supportURL": 1024, "privacyPolicyURL": 1024,
"termsOfServiceURL": 1024}
# Anthropic's directory reads these from .claude-plugin/plugin.json; each matches an OpenAI interface field.
CLAUDE_LISTING = {"supportUrl": "supportURL", "privacyPolicyUrl": "privacyPolicyURL",
"termsOfServiceUrl": "termsOfServiceURL"}


class ReferenceProblems(AssertionError):
Expand Down Expand Up @@ -54,6 +66,68 @@ def docs_url_problems(root):
return problems


def prose_words(text):
"""Count words outside fenced code, as Anthropic's directory counts README words."""
words, is_fenced = 0, False
for line in text.splitlines():
if line.lstrip().startswith(("```", "~~~")):
is_fenced = not is_fenced
elif not is_fenced:
words += len(line.split())
return words


def validate_listings(root=ROOT):
"""Check the directory listing metadata: manifests, plugin README, skills.sh, and MCP Registry entries."""
plugin = root / "voiceflow"
portable = json.loads((plugin / "plugin.json").read_text(encoding="utf-8"))
claude, codex, cursor = (json.loads((plugin / folder / "plugin.json").read_text(encoding="utf-8"))
for folder in (".claude-plugin", ".codex-plugin", ".cursor-plugin"))
for manifest in (portable, claude, codex, cursor):
for field in ("homepage", "repository", "keywords"):
assert manifest.get(field) == portable[field], f"Listing field differs between manifests: {field}"
assert portable["homepage"].startswith("https://") and portable["repository"].startswith("https://github.com/")
assert portable["keywords"] and all(KEYWORD.fullmatch(word) for word in portable["keywords"]), "Keywords must be lowercase-hyphenated"
openai = portable["extensions"]["com.openai"]
interface = openai["interface"]
for field, limit in INTERFACE_LIMITS.items():
assert isinstance(interface.get(field), str) and 0 < len(interface[field]) <= limit, f"Invalid interface.{field}"
if field.endswith("URL"):
assert interface[field].startswith("https://"), f"interface.{field} must be https"
capabilities, prompts = interface["capabilities"], interface["defaultPrompt"]
assert len(capabilities) <= 20 and all(0 < len(item) <= 120 for item in capabilities), "Invalid capabilities"
assert 0 < len(prompts) <= 3 and len(set(prompts)) == len(prompts), "Use one to three unique default prompts"
assert all(0 < len(prompt) <= 128 and "@" not in prompt for prompt in prompts), "Invalid default prompt"
onboarding = openai["onboardingSkill"]
assert onboarding.startswith("./") and (plugin / onboarding).is_file(), "Onboarding skill missing"
assert codex["interface"] == interface, "Codex interface differs from plugin.json"
assert codex["extensions"]["com.openai"]["onboardingSkill"] == onboarding, "Codex onboarding skill differs"
assert claude["displayName"] == interface["displayName"], "Claude displayName differs"
assert claude["documentationUrl"].startswith("https://"), "documentationUrl must be https"
for field, openai_field in CLAUDE_LISTING.items():
assert claude[field] == interface[openai_field], f"Claude {field} differs from interface.{openai_field}"
icons = [claude.get("icon"), cursor.get("logo"), interface.get("logo"), interface.get("composerIcon")]
for icon in filter(None, icons):
assert (plugin / icon).is_file() and icon.endswith((".png", ".jpg", ".jpeg")), f"Icon file missing: {icon}"
market = json.loads((root / ".agents/plugins/marketplace.json").read_text(encoding="utf-8"))
assert market["plugins"][0]["category"] == interface["category"], "Marketplace category differs"
readme = plugin / "README.md"
assert readme.is_file() and prose_words(readme.read_text(encoding="utf-8")) >= 40, "Plugin README needs 40+ words"
skills_sh = json.loads((root / "skills.sh.json").read_text(encoding="utf-8"))
assert skills_sh == {"$schema": SKILLS_SH_SCHEMA, "paths": ["./voiceflow/skills"]}, "skills.sh.json drift"
# Registry users add the docs server themselves, so its entry omits the package's logging option.
expected = {ACCOUNT_MCP_URL, DOCS_MCP_URL.split("?")[0]}
entries = [json.loads(path.read_text(encoding="utf-8"))
for path in sorted((root / "mcp-registry").glob("*/server.json"))]
assert {remote["url"] for entry in entries for remote in entry["remotes"]} == expected, "Registry URLs differ"
assert len({entry["name"] for entry in entries}) == len(entries) == len(expected), "One registry entry per server"
for entry in entries:
assert entry["$schema"] == REGISTRY_SCHEMA and SEMVER.fullmatch(entry["version"])
assert re.fullmatch(r"com\.voiceflow/[a-z0-9-]+", entry["name"]), "Registry names use com.voiceflow/"
assert 0 < len(entry["description"]) <= 100 and 0 < len(entry["title"]) <= 100
assert [remote["type"] for remote in entry["remotes"]] == ["streamable-http"]


def validate(root=ROOT):
plugin = root / "voiceflow"
manifests = [plugin / "plugin.json"] + [
Expand All @@ -70,7 +144,7 @@ def validate(root=ROOT):
assert knowledge.startswith("---\n"), "Missing skill package metadata"
record = dict(line.split(": ", 1) for line in knowledge.split("---", 2)[1].strip().splitlines())
assert record == {"package": data[0]["name"], "version": data[0]["version"]}, "Skill package version differs"
assert len(data[0]["extensions"]["com.openai"]["interface"]["shortDescription"]) <= 30
validate_listings(root)
portable = json.loads((plugin / "mcp.json").read_text(encoding="utf-8"))
legacy = json.loads((plugin / ".mcp.json").read_text(encoding="utf-8"))
assert portable["$schema"] == "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json"
Expand Down
4 changes: 4 additions & 0 deletions skills.sh.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "https://skills.sh/schemas/skills.sh.schema.json",
"paths": ["./voiceflow/skills"]
}
49 changes: 49 additions & 0 deletions tests/test_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,55 @@ def test_docs_mcp_url_must_turn_off_logging(self):
self.assertEqual(validator.docs_url_problems(root), [problem, problem])


class ListingTests(unittest.TestCase):
def setUp(self):
scratch = tempfile.TemporaryDirectory()
self.addCleanup(scratch.cleanup)
self.root = Path(scratch.name)
for name in ("voiceflow", ".agents", "mcp-registry"):
shutil.copytree(ROOT / name, self.root / name, ignore=shutil.ignore_patterns("__pycache__"))
shutil.copyfile(ROOT / "skills.sh.json", self.root / "skills.sh.json")

def test_current_listing_metadata_passes(self):
validator.validate_listings(self.root)

def test_listing_drift_is_rejected(self):
def interface(data):
return data["extensions"]["com.openai"]["interface"]

cases = [
("voiceflow/.codex-plugin/plugin.json",
lambda data: data["interface"].update(category="Development"), "Codex interface differs"),
("voiceflow/.claude-plugin/plugin.json",
lambda data: data.update(privacyPolicyUrl="https://example.invalid/privacy"), "privacyPolicyUrl"),
("voiceflow/plugin.json",
lambda data: interface(data)["defaultPrompt"].__setitem__(0, "x" * 129), "default prompt"),
("voiceflow/plugin.json",
lambda data: interface(data).update(shortDescription="x" * 31), "shortDescription"),
("voiceflow/.cursor-plugin/plugin.json", lambda data: data.pop("keywords"), "keywords"),
("voiceflow/.claude-plugin/plugin.json", lambda data: data.update(icon="./icon.png"), "Icon file missing"),
("mcp-registry/knowledge/server.json",
lambda data: data["remotes"][0].update(url=validator.DOCS_MCP_URL), "Registry URLs differ"),
("skills.sh.json", lambda data: data.update(paths=["./skills"]), "skills.sh.json"),
]
for name, change, message in cases:
with self.subTest(name=name, message=message):
path = self.root / name
original = path.read_bytes()
data = json.loads(original)
change(data)
path.write_text(json.dumps(data), encoding="utf-8")
with self.assertRaisesRegex(AssertionError, message):
validator.validate_listings(self.root)
path.write_bytes(original)

def test_plugin_readme_counts_prose_words_only(self):
readme = self.root / "voiceflow/README.md"
readme.write_text("# Voiceflow\n\nShort intro.\n\n```text\n" + "word " * 60 + "\n```\n", encoding="utf-8")
with self.assertRaisesRegex(AssertionError, "40\\+ words"):
validator.validate_listings(self.root)


class ReleaseTests(unittest.TestCase):
def setUp(self):
self.scratch = tempfile.TemporaryDirectory()
Expand Down
4 changes: 2 additions & 2 deletions tests/test_skill_references.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,9 @@ def test_validate_lists_every_problem_and_exits_nonzero(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary) / "repo"
for folder in ("voiceflow", "scripts", "tests/fixtures", "docs", "evals",
".claude-plugin", ".cursor-plugin", ".agents"):
".claude-plugin", ".cursor-plugin", ".agents", "mcp-registry"):
shutil.copytree(ROOT / folder, root / folder, ignore=shutil.ignore_patterns("__pycache__"))
for document in [*ROOT.glob("*.md"), ROOT / "LICENSE"]:
for document in [*ROOT.glob("*.md"), ROOT / "LICENSE", ROOT / "skills.sh.json"]:
shutil.copyfile(document, root / document.name)
skill = root / "voiceflow/skills/voiceflow-build/SKILL.md"
lines = skill.read_text(encoding="utf-8").splitlines()
Expand Down
18 changes: 17 additions & 1 deletion voiceflow/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
{
"name": "voiceflow",
"displayName": "Voiceflow",
"version": "1.0.0",
"description": "Connect to Voiceflow, then build and improve agents with shared product guidance.",
"author": {
"name": "Voiceflow, Inc."
},
"license": "MIT"
"homepage": "https://www.voiceflow.com/docs",
"repository": "https://github.com/voiceflow/agent-plugins",
"license": "MIT",
"keywords": [
"voiceflow",
"agents",
"ai-agents",
"chatbots",
"voice",
"workflows",
"mcp"
],
"documentationUrl": "https://www.voiceflow.com/docs",
"supportUrl": "https://github.com/voiceflow/agent-plugins/blob/main/SUPPORT.md",
"privacyPolicyUrl": "https://www.voiceflow.com/privacy",
"termsOfServiceUrl": "https://www.voiceflow.com/legal/terms"
}
38 changes: 37 additions & 1 deletion voiceflow/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,43 @@
"author": {
"name": "Voiceflow, Inc."
},
"homepage": "https://www.voiceflow.com/docs",
"repository": "https://github.com/voiceflow/agent-plugins",
"license": "MIT",
"keywords": [
"voiceflow",
"agents",
"ai-agents",
"chatbots",
"voice",
"workflows",
"mcp"
],
"skills": "./skills/",
"mcpServers": "./.mcp.json",
"license": "MIT"
"interface": {
"displayName": "Voiceflow",
"shortDescription": "Build with Voiceflow",
"longDescription": "Connect to Voiceflow, then build, debug, and improve agents using portable product skills. Setup connects your Voiceflow account through the Voiceflow CLI or the hosted Voiceflow MCP server, with browser sign-in, and adds the public Voiceflow docs server. Eighteen skills cover building agents, prompts and playbooks, workflows, functions, integrations, knowledge bases, messages, voice, phone numbers, the web chat widget, debugging, transcripts, evaluations, and environments. Changes to your account follow your requests and your assistant's approval prompts. You need a Voiceflow account, and your plan's limits apply.",
"developerName": "Voiceflow, Inc.",
"category": "Developer Tools",
"capabilities": [
"Read",
"Write"
],
"websiteURL": "https://www.voiceflow.com",
"supportURL": "https://github.com/voiceflow/agent-plugins/blob/main/SUPPORT.md",
"privacyPolicyURL": "https://www.voiceflow.com/privacy",
"termsOfServiceURL": "https://www.voiceflow.com/legal/terms",
"defaultPrompt": [
"Connect me to Voiceflow and check that my account works.",
"Find out why my Voiceflow agent answers twice after a playbook.",
"Add the Voiceflow chat widget to my website."
]
},
"extensions": {
"com.openai": {
"onboardingSkill": "./skills/voiceflow-setup/SKILL.md"
}
}
}
15 changes: 13 additions & 2 deletions voiceflow/.cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@
"author": {
"name": "Voiceflow, Inc."
},
"skills": "./skills/",
"license": "MIT"
"homepage": "https://www.voiceflow.com/docs",
"repository": "https://github.com/voiceflow/agent-plugins",
"license": "MIT",
"keywords": [
"voiceflow",
"agents",
"ai-agents",
"chatbots",
"voice",
"workflows",
"mcp"
],
"skills": "./skills/"
}
Loading
Loading