Skip to main content

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 needs licenseDeclaration).
  • spec.compatibility: platform and sdk ranges 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 a reason; mark sensitive: true what 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/*.rego with _test.rego (/tmp/toolchains/opa test policies/opa), policies/openfga/model.fga and tests.fga.yaml (/tmp/toolchains/fga model test --tests ...). OPA policies are installed as drafts.
  • evaluations/promptfoo.yaml (exec: provider, deterministic assertions) and evaluations/ragas.jsonl (question, ground_truth, reference_contexts) scored over rag/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): a dashboards folder under observability is an error; the dashboards live in the Navin Manager and the portal.
  • tests/unit, tests/integration, tests/authz, tests/security: pytest suites run by plugin publish with the pack interpreter; the counts are attached to the publication and checked by the tests gate.

Checklist before publishing​

  • enterprise-ai plugin validate is 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 test and fga model test pass; evaluations reach their thresholds.
  • plugin sign then plugin publish with 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.

  1. 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.lock
    packs/bootstrap.sh

    Expected result: the created files are listed and the pack interpreter is installed.

    If it fails: exit code 1 means the folder exists; add --force.

  2. 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.

  3. Validate. The same checks run again on the Control Plane at publication and installation.

    enterprise-ai plugin validate packs/my-support

    Expected result: the pack is reported valid with its name and version.

    If it fails: exit code 1 lists every layout and manifest error.

  4. Test. Run the four pytest kinds, then the OPA and OpenFGA tests.

    /tmp/toolchains/packs-venv/bin/python -m pytest -q packs/my-support/tests
    cd packs/my-support
    enterprise-ai test authz

    Expected result: every suite passes.

    If it fails: enterprise-ai test returns 1 for a failed suite or a missing opa or fga binary.

  5. Build and sign. build writes the archive, its SHA-256 digest and the SPDX SBOM; sign signs the archive and every SKILL.md and writes the descriptor <name>-<version>.pack.json.

    enterprise-ai plugin build packs/my-support --out dist
    enterprise-ai plugin sign packs/my-support --out dist --key cosign.key --signer acme

    acme: 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".

  6. Publish. publish runs 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-support

    Expected result: the version appears in Marketplace > Pack catalog.

    If it fails: exit code 1 names the failed gate; 422 publication_gate_failed or 409 marketplace_version_exists come from the Control Plane.

  7. Preflight and install. POST /v1/marketplace/preflight runs every installation check without installing. plugin install then 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.read

    Expected result: the approval id and its quorum are printed; after approval and apply, the installation is installed in Marketplace > Installations.

    If it fails: 403 permission_unsafe, 422 config_invalid, 422 permission_not_requested, 409 package_suspended, 422 preflight_failed, 422 smoke_failed (everything is rolled back).

  8. 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 (not installed, or version not higher), 409 rollback_unavailable.

  9. 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 suspended or uninstalled; the answer of uninstall lists removed.

    If it fails: 409 installation_state for a state that does not allow the change.

Troubleshooting​

SymptomCauseFix
plugin validate reports a missing scopeAn agent, skill or tool uses a scope that is not requested.Add it to spec.requestedPermissions with a reason.
403 permission_unsafeThe pack requests a scope the platform refuses.Remove the scope.
422 permission_not_requestedThe installer granted a scope the pack does not request.Grant only requested scopes.
422 config_invalidThe installation configuration does not match spec.configSchema.Fix the --config values.
422 preflight_failedA preflight check failed (compatibility, dependency, digest, signature, secret reference, model).Read every check of POST /v1/marketplace/preflight.
422 smoke_failedThe smoke test failed after apply.The installation was rolled back; fix and publish a new version.
403 permission_not_granted at run timeThe MCP server asked authorize for a scope that was not granted.Upgrade the installation with the grant, or remove the call.
409 marketplace_version_existsThe version is already published.Raise metadata.version.

Next steps​