Skip to content

Latest commit

 

History

History
453 lines (350 loc) · 14.9 KB

File metadata and controls

453 lines (350 loc) · 14.9 KB

Adding MCP Servers to Dockyard

This guide walks you through packaging and contributing your MCP server to Dockyard.

Overview

Adding your MCP server is simple:

  1. Create a spec.yaml configuration file
  2. Submit a pull request
  3. CI/CD automatically builds, scans, and publishes your container

Directory Structure

{protocol}/{server-name}/spec.yaml

Choose the protocol directory based on your package type:

  • npx/ - Node.js packages from npm
  • uvx/ - Python packages from PyPI
  • go/ - Go modules

spec.yaml Reference

# {Server Name} MCP Server Configuration
# Package: {package-registry-url}
# Repository: {source-repository-url}
# Will build as: ghcr.io/stacklok/dockyard/{protocol}/{name}:{version}

metadata:
  name: your-server-name           # Required: Unique server name
  description: "Brief description" # Optional: What does your server do?
  version: "1.0.0"                 # Optional: Server version
  protocol: npx                    # Required: npx, uvx, or go

spec:
  package: "your-package-name"     # Required: Package name from registry
  version: "1.0.0"                 # Required: Specific version to build
  args:                            # Optional: CLI arguments for the package
    - "arg1"                       # Passed to the entrypoint command
    - "arg2"
  env:                             # Optional: env vars baked into the runtime image
    SOME_VAR: "some-value"         # Present in the running container, not just at build time

  # Optional (npx only): force pinned versions of transitive npm dependencies.
  # Injected as an "overrides" block in the generated package.json. Each entry
  # requires a reason so the justification is auditable in-repo.
  overrides:
    - package: "@modelcontextprotocol/sdk"
      version: "1.26.0"
      reason: "Upstream hard-pins a vulnerable version; this same-major bump fixes it."

  # Optional (uvx only): force pinned versions of transitive Python dependencies.
  # Written to a uv overrides requirements file and passed to `uv tool install
  # --overrides`. Each entry requires a reason.
  constraints:
    - spec: "fastmcp>=3.2.0"
      reason: "Upstream caps the dependency below the version that fixes a CVE."

provenance:                        # Optional but recommended
  repository_uri: "https://github.com/user/repo"
  repository_ref: "refs/tags/v1.0.0"

  # Attestation information (if available)
  attestations:
    available: true                # Whether the package has provenance attestations
    verified: true                 # Whether you've verified the attestations
    publisher:
      kind: "GitHub"              # Publisher type (e.g., GitHub, GitLab)
      repository: "user/repo"     # Publisher repository
      workflow: "release.yml"     # Publishing workflow (optional)

# Optional: Security scan configuration
security:
  # Allowlist for known false positives or acceptable issues
  allowed_issues:
    - code: "AITech-1.1"
      reason: "Explanation of why this issue is acceptable"
  # Mock environment variables for servers that require them during scanning
  mock_env:
    - name: API_URL
      value: "https://mock-api.example.com"
      description: "Required for server startup - mock value for scanning"

Protocol-Specific Examples

Node.js (npx)

# NPM package: https://www.npmjs.com/package/@your-org/mcp-server
metadata:
  name: my-node-server
  description: "My awesome Node.js MCP server"
  version: "2.1.0"
  protocol: npx

spec:
  package: "@your-org/mcp-server"  # NPM package name
  version: "2.1.0"

provenance:
  repository_uri: "https://github.com/your-org/mcp-server"
  repository_ref: "refs/tags/v2.1.0"

Some packages require specific CLI arguments:

# LaunchDarkly requires the "start" command
spec:
  package: "@launchdarkly/mcp-server"
  version: "0.4.2"
  args:
    - "start"
# Results in: ENTRYPOINT ["npx", "@launchdarkly/mcp-server", "start"]

Python (uvx)

# PyPI package: https://pypi.org/project/your-mcp-server/
metadata:
  name: my-python-server
  description: "My awesome Python MCP server"
  version: "1.5.2"
  protocol: uvx

spec:
  package: "your-mcp-server"       # PyPI package name
  version: "1.5.2"

provenance:
  repository_uri: "https://github.com/your-org/python-mcp-server"
  repository_ref: "refs/tags/v1.5.2"

Go

# Go module: go get github.com/your-org/go-mcp-server
metadata:
  name: my-go-server
  description: "My awesome Go MCP server"
  version: "0.3.1"
  protocol: go

spec:
  package: "github.com/your-org/go-mcp-server"  # Go module path
  version: "v0.3.1"                              # Go version tag

provenance:
  repository_uri: "https://github.com/your-org/go-mcp-server"
  repository_ref: "refs/tags/v0.3.1"

Dependency Overrides and Constraints

Sometimes a package pins or caps a transitive dependency to a version that fails the build-containers Grype gate (--fail-on high --only-fixed), and the fix lives in a version excluded by that pin/cap. Dockyard can force a different resolved version of the offending dependency without forking the upstream package.

Every entry must include a reason (validation fails otherwise) so the justification for circumventing an upstream pin is auditable in-repo, mirroring security.allowed_issues.

npx: spec.overrides

For npx servers, spec.overrides is injected as an npm overrides block in the generated package.json, so npm resolves the pinned version regardless of the upstream-declared range:

metadata:
  name: brightdata-mcp
  protocol: npx
spec:
  package: "@brightdata/mcp"
  version: "2.9.5"
  overrides:
    - package: "@modelcontextprotocol/sdk"
      version: "1.26.0"
      reason: |
        @brightdata/mcp hard-pins @modelcontextprotocol/sdk 1.21.2 (3x HIGH);
        fixes are >=1.24. 1.26.0 is same-major, so no API break.

This rewrites the package.json step in the Dockerfile to:

RUN echo '{"name":"mcp-container","overrides":{"@modelcontextprotocol/sdk":"1.26.0"},"version":"1.0.0"}' > package.json

uvx: spec.constraints

For uvx servers, each spec.constraints[].spec is a PEP 508 requirement written to a uv overrides requirements file and passed to uv tool install --overrides, forcing the resolved version even when upstream caps it:

metadata:
  name: mcp-clickhouse
  protocol: uvx
spec:
  package: "mcp-clickhouse"
  version: "0.3.0"
  constraints:
    - spec: "fastmcp>=3.2.0"
      reason: |
        mcp-clickhouse caps fastmcp <3.0.0, but the CRITICAL CVE-2026-32871 fix
        is fastmcp 3.2.0.

This injects an overrides file and rewrites the install step in the Dockerfile to:

RUN printf '%s\n' \
    'fastmcp>=3.2.0' \
    > /tmp/uv-overrides.txt
RUN package="mcp-clickhouse@0.3.0"; \
    package_spec=$(echo "$package" | sed 's/@/==/'); \
    uv tool install --overrides /tmp/uv-overrides.txt "$package_spec" && \
    ls -la /opt/uv-tools/bin/

Caution: Forcing a version across an upstream's deliberate cap can cross a major version boundary (e.g. fastmcp 2.x → 3.x) and break the server's tools at runtime even when the image builds and the package imports. Functionally test the server before relying on such an override.

How overrides interact with the security scan

The mcp-security-scan CI job runs the package directly (uvx <pkg> / npx <pkg>) rather than the built image, so it does not automatically inherit anything injected into the Dockerfile. scripts/mcp-scan reapplies both kinds of override so the scanned process resolves the same dependency versions the image ships:

  • uvx constraints are written to a uv overrides requirements file and passed as uvx --overrides <file>.
  • npx overrides cannot be passed on the command line, because npm honors overrides only from a package.json it installs into. The scan stages a throwaway project containing the server package plus the overrides block, runs npm install in it, and runs the scanner with that directory as its working directory so npx resolves the installed tree. npx --no-install is passed as well, so npx fails rather than silently fetching an un-overridden copy of the package.

Beyond matching what ships, this means the scan doubles as a check on the override itself: an override that breaks the server's startup shows up as a scan failure before the image is published, rather than as a broken container afterwards.

Note that servers with security.insecure_ignore: true (typically those needing real credentials to start) cannot be meaningfully scanned either way, so overrides have no observable effect on their scan.

Step-by-Step Process

1. Find Package Information

# Python
curl -s https://pypi.org/pypi/{package}/json | jq -r '.info.version'

# Node.js
npm view {package} version

2. Create Directory Structure

mkdir -p {protocol}/{server-name}

3. Create spec.yaml

Use the template above, filling in your package details.

4. Verify Provenance (Recommended)

Check if your package has provenance attestations:

# Build dockhand first
go build -o build/dockhand ./cmd/dockhand

# Verify provenance
./build/dockhand verify-provenance -c {protocol}/{server-name}/spec.yaml -v

Document any provenance information in your spec.yaml. See Package Provenance for details.

5. Test Locally

# Validate spec and generate Dockerfile
task build -- {protocol}/{server-name}

# Run security scan
task scan -- {protocol}/{server-name}

# Optional: Build and test container image
task test-build -- {protocol}/{server-name}

6. Submit Pull Request

  1. Fork the repository
  2. Create your server directory and spec.yaml
  3. Submit a PR with:
    • Clear title: "Add [Your Server Name] MCP server"
    • Description of what your server does
    • Link to the package registry and source repository

CI/CD Pipeline

When you submit a PR, the following happens automatically:

graph TD
    A[Discover Configs] --> B[Verify Provenance]
    B --> C[MCP Security Scan]
    C --> D{Scan Passed?}
    D -->|Yes| E[Build Containers]
    D -->|No| F[Fail Build]
    E --> G[Trivy Vulnerability Scan]
    G --> H[Sign with Cosign]
    H --> I[Create Attestations]
    I --> J[Push to Registry]
Loading

Security Scanning

All MCP servers are scanned for security vulnerabilities using mcp-scanner. The scan is blocking - your PR cannot merge if it fails.

If your scan finds issues that are false positives or acceptable in your context, add them to the security allowlist:

security:
  allowed_issues:
    - code: "AITech-1.1"
      reason: "Tool description contains imperative instructions necessary for proper operation"
    - code: "AITech-9.1"
      reason: "Destructive flow mitigated by container sandboxing"

Servers Requiring Environment Variables During Scanning

Some MCP servers require environment variables to start (e.g., API URLs, tokens). Since the security scanner needs to start the server to discover its tools, you can provide mock values that allow the server to start without functional credentials:

security:
  mock_env:
    - name: SEARXNG_URL
      value: "https://mock-searxng.example.com"
      description: "SearXNG instance URL - mock for scanning"
    - name: API_TOKEN
      value: "mock-token-for-scanning-00000000"
      description: "API token - mock value, not a real credential"

Important notes about mock_env:

  • Mock values are not secrets - they are committed to the repository
  • Values should be obviously fake (use mock-, placeholder UUIDs, example.com domains)
  • Purpose is to allow server startup for scanning, not functional operation
  • Only consumed by the scan tooling (scripts/mcp-scan/) - it is not wired into the built container
  • Servers still need to pass security scans or allowlist known issues

See Security Overview for more details on what we scan for.

Servers Requiring Environment Variables at Runtime

security.mock_env above only affects scanning. If your server actually needs an environment variable set in the shipped, running container - for example, to select a non-default backend when a dependency assumes capabilities the container doesn't have - use spec.env instead:

spec:
  package: "your-package-name"
  version: "1.0.0"
  env:
    PYTHON_KEYRING_BACKEND: "keyrings.alt.file.PlaintextKeyring"

This is baked into the final stage of the generated Dockerfile as an ENV instruction, so it's present in the process environment every time the container runs - not just during the build. uvx/okta/spec.yaml uses this for exactly this reason: okta-mcp-server caches OAuth tokens via Python's keyring library, which otherwise tries to reach an OS secret-service/D-Bus backend that doesn't exist in the container.

Don't use spec.env to embed secrets or credentials - it's committed to the repository just like the rest of spec.yaml. It's for non-sensitive configuration values only.

After Merge

Once merged:

  • Container published to ghcr.io/stacklok/dockyard/{protocol}/{name}:{version}
  • Renovate automatically creates PRs for version updates
  • Trivy scans run weekly to catch new CVEs

Local Development

Build dockhand CLI

go build -o build/dockhand ./cmd/dockhand

Generate Dockerfile

# Generate to stdout
./build/dockhand build -c npx/context7/spec.yaml

# Save to file
./build/dockhand build -c npx/context7/spec.yaml -o Dockerfile

Build with Custom Tag

./build/dockhand build -c npx/context7/spec.yaml -t my-custom-tag:latest

CLI Flags

Flag Description
-c, --config YAML spec file (required)
-o, --output Output file (default: stdout)
-t, --tag Custom image tag
-v, --verbose Verbose output
--check-provenance Require provenance verification
--warn-no-provenance Warn if no provenance (default: true)

Troubleshooting

Issue Solution
Package not found Verify exact package name in registry
Build fails Check Dockerfile syntax with dockhand build -c spec.yaml
Version error Ensure version exists in package registry
Wrong protocol Verify package type matches directory (uvx/npx/go)
Security scan fails Review issues, allowlist false positives with explanation

Key Rules

  1. Always use exact versions - no ranges or "latest" tags
  2. Test locally before committing
  3. Include all metadata fields
  4. Use correct protocol directory (uvx/npx/go)
  5. Reference official package registries in comments
  6. Verify provenance when available

What's Next?