Pack development guide: plugins, MCP servers, skills, prompts and connectors
French version: ../../fr/guides/pack-development.md
This guide shows how to write each part of a complete pack. Start from a scaffold, which is runnable and passes every gate:
enterprise-ai plugin create packs/my-support --domain support --name my-support --lock packs/requirements.lock
packs/bootstrap.sh
/tmp/toolchains/packs-venv/bin/python -m pytest -q packs/my-support/tests
The reference packs packs/hr and packs/finance are complete examples. Text rules never replace a security
mechanism: the platform enforces authorization (granted permissions, OPA, OpenFGA, tool gateway).
Plugin manifest (plugin.yaml)
metadata:name(DNS like),version(semver, immutable once published),owner.spec.format: pack,spec.license(MIT or Apache-2.0; anything else needslicenseDeclaration).spec.compatibility:platformandsdkranges such as">=1.0.0 <2.0.0",runtimes: [navin].spec.dependencies:[{name, version range, optional}], checked against the packs installed in the same environment.spec.requestedPermissions: one entry per scope with areason; marksensitive: truewhat writes, pays or reads restricted data. Every scope used by an agent, a skill or a tool must be listed. Unsafe scopes are refused.spec.dataProtection,spec.approvals,spec.docs {en, fr},spec.evaluations,spec.tests.
Validate at any time with enterprise-ai plugin validate <folder>; the Control Plane re-runs the same checks.
Agents and subagents
Agent manifests (kind: Agent, contracts/v1/agent-manifest.schema.json) live in agents/ and subagents/. Refer to
the pack objects by their pack name (tool:hr-leave-balance, skill:hr-leave-policy@1.0.0); the installer renames
them per environment and keeps the granted scopes only. The code (agents/assistant.py) builds a Navin
AgentRunner whose local tools call the MCP server, with the sensitive tools in approval_for so that the runner
asks for a human decision first. Local tool names use dashes (leave-balance).
Skills (skills/<name>/SKILL.md)
Front matter: name, description, version, permissions {scopes, data_classification_max, network, filesystem}, then the instructions in Markdown. Quote a description that contains a colon. At build time
plugin sign signs the SHA-256 of each SKILL.md; at install the Skill Registry verifies it and the Skill Loader
refuses the skill to an agent that was not granted its scopes.
Prompts (prompts/*.yaml)
{name, description, template, variables} with {{variable}} placeholders and typed variables (string,
integer, number, boolean, array, object, enum, max_length, default, required). They are published
in the Prompt Registry as <name>-<environment>; rendering refuses missing, mistyped or unknown variables.
MCP servers (mcp/servers, mcp/tools, mcp/schemas)
Use the official MCP Python SDK (package mcp, MIT licence, pinned in packs/requirements.lock):
from mcp.server.mcpserver import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
def build_server(adapter, authorize):
server = MCPServer(name="my-mcp", version="1.0.0")
@server.tool(name="my_search", description="Search records (read).")
def search(query: str, limit: int = 5) -> dict:
validate("my-search", {"query": query, "limit": limit}) # mcp/schemas/my-search.input.json
if not authorize("my.records.read"): # POST /v1/marketplace/installations/{id}/authorize
raise ToolError("permission my.records.read not granted")
return {"items": adapter.search(query, limit)}
return server
Rules: validate the input against the published schema, ask authorize before touching the connector, raise
ToolError for a refusal (the message reaches the caller), never return secrets or bank details, treat every
input as data. Declare each tool in mcp/tools/*.yaml (kind: Tool, endpoint.type: mcp, mcp_server,
scopes_required, data_classification, requires_approval for side effects) with the same input schema.
Connectors (connectors/)
Declare the connector (kind: Connector, auth.credential_ref: openbao://..., resources and classifications) and
write three classes in connectors/adapter.py:
- an interface (
typing.Protocol) used by the MCP server; - a simulated implementation reading fixtures labelled
"fictitious": true; - a brokered implementation that only calls
call(method, path, body, headers)of the platform Connector Broker, which resolves the credential from the secret reference of the installation and applies the egress policy.
Side effecting operations take an idempotency key: a replay returns the first result, and the business rule
forbids a second execution (see packs/finance/connectors/adapter.py, payment exactly once under concurrent
retries).
Policies, evaluations, observability, tests
policies/opa/*.regowith_test.rego(/tmp/toolchains/opa test policies/opa),policies/openfga/model.fgaandtests.fga.yaml(/tmp/toolchains/fga model test --tests ...). OPA policies are installed as drafts.evaluations/promptfoo.yaml(exec:provider, deterministic assertions) andevaluations/ragas.jsonl(question,ground_truth,reference_contexts) scored overrag/retrieve.py.observability/alerts/*.yaml(Prometheus rule groups and SLO) on the platform metrics (eai_http_requests_total,eai_policy_decisions_total). A pack ships no dashboard (ADR-0169): adashboardsfolder underobservabilityis an error; the dashboards live in the Navin Manager and the portal.tests/unit,tests/integration,tests/authz,tests/security: pytest suites run byplugin publishwith the pack interpreter; the counts are attached to the publication and checked by thetestsgate.
Checklist before publishing
-
enterprise-ai plugin validateis clean; English and French docs; no em or en dash. - Every scope used is requested, sensitive ones are marked; no unsafe scope.
- Fixtures labelled fictitious; no secret, key or real personal data in the pack.
- The four pytest kinds,
opa testandfga model testpass; evaluations reach their thresholds. -
plugin signthenplugin publishwith the OCI reference of the customer registry.
From scaffold to installation
Each step below names what it does, why, the command or screen, the expected result and what to do when it fails.
The pack is my-support; build, sign and publish take the pack folder and rebuild the same reproducible archive
in the --out folder.
-
Create the pack. The scaffold already passes every gate, so each later failure comes from your change.
enterprise-ai plugin create packs/my-support --domain support --name my-support --lock packs/requirements.lockpacks/bootstrap.shExpected result: the created files are listed and the pack interpreter is installed.
If it fails: exit code 1 means the folder exists; add
--force. -
Write the parts. Fill
plugin.yaml, then the agents, skills, prompts, MCP servers, connectors and policies as described in the sections above.Expected result: every scope used in the pack is in
spec.requestedPermissions.If it fails: the next step names each missing or unsafe scope.
-
Validate. The same checks run again on the Control Plane at publication and installation.
enterprise-ai plugin validate packs/my-supportExpected result: the pack is reported valid with its name and version.
If it fails: exit code 1 lists every layout and manifest error.
-
Test. Run the four pytest kinds, then the OPA and OpenFGA tests.
/tmp/toolchains/packs-venv/bin/python -m pytest -q packs/my-support/testscd packs/my-supportenterprise-ai test authzExpected result: every suite passes.
If it fails:
enterprise-ai testreturns 1 for a failed suite or a missingopaorfgabinary. -
Build and sign.
buildwrites the archive, its SHA-256 digest and the SPDX SBOM;signsigns the archive and every SKILL.md and writes the descriptor<name>-<version>.pack.json.enterprise-ai plugin build packs/my-support --out distenterprise-ai plugin sign packs/my-support --out dist --key cosign.key --signer acmeacme: the publisher name registered in the Marketplace.Expected result: the digest, the signer and the number of signed skills are printed.
If it fails: exit code 2 with "--key and --signer are required".
-
Publish.
publishruns the tests, the evaluations and the scan, pushes to the customer registry with--oci, then calls POST /v1/marketplace/packages.enterprise-ai plugin publish packs/my-support --out dist --key cosign.key --oci <harbor-host>/packs/my-supportExpected result: the version appears in Marketplace > Pack catalog.
If it fails: exit code 1 names the failed gate; 422
publication_gate_failedor 409marketplace_version_existscome from the Control Plane. -
Preflight and install. POST /v1/marketplace/preflight runs every installation check without installing.
plugin installthen opens the installation request and its approval; once approved, POST /v1/marketplace/installations/{installation_id}/apply creates the objects and runs a smoke test.enterprise-ai plugin install my-support --version 1.0.0 --env staging --grant my.records.readExpected result: the approval id and its quorum are printed; after approval and apply, the installation is
installedin Marketplace > Installations.If it fails: 403
permission_unsafe, 422config_invalid, 422permission_not_requested, 409package_suspended, 422preflight_failed, 422smoke_failed(everything is rolled back). -
Upgrade or roll back. Publish a higher version, then POST /v1/marketplace/installations/{installation_id}/upgrade and apply it once approved; POST /v1/marketplace/installations/{installation_id}/rollback restores the previous version with its previous grant.
Expected result: the installation runs the new version.
If it fails: 409
installation_state(notinstalled, or version not higher), 409rollback_unavailable. -
Suspend or uninstall. POST /v1/marketplace/installations/{installation_id}/status suspends or resumes the installation; POST /v1/marketplace/installations/{installation_id}/uninstall removes every object it created.
Expected result: status
suspendedoruninstalled; the answer of uninstall listsremoved.If it fails: 409
installation_statefor a state that does not allow the change.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
plugin validate reports a missing scope | An agent, skill or tool uses a scope that is not requested. | Add it to spec.requestedPermissions with a reason. |
403 permission_unsafe | The pack requests a scope the platform refuses. | Remove the scope. |
422 permission_not_requested | The installer granted a scope the pack does not request. | Grant only requested scopes. |
422 config_invalid | The installation configuration does not match spec.configSchema. | Fix the --config values. |
422 preflight_failed | A preflight check failed (compatibility, dependency, digest, signature, secret reference, model). | Read every check of POST /v1/marketplace/preflight. |
422 smoke_failed | The smoke test failed after apply. | The installation was rolled back; fix and publish a new version. |
403 permission_not_granted at run time | The MCP server asked authorize for a scope that was not granted. | Upgrade the installation with the grant, or remove the call. |
409 marketplace_version_exists | The version is already published. | Raise metadata.version. |
Next steps
- Marketplace guide to operate the installations.
- Plugin development guide, skill development guide and connector development guide.
- MCP development guide and policy development guide.