Skip to content
Closed
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
19 changes: 12 additions & 7 deletions cmd/dockhand/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@ type MCPServerMetadata struct {

// MCPServerPackageSpec defines the package to be containerized
type MCPServerPackageSpec struct {
Package string `yaml:"package"` // e.g., "@upstash/context7-mcp"
Version string `yaml:"version,omitempty"` // e.g., "1.0.14"
Args []string `yaml:"args,omitempty"` // Additional arguments for the package
Env map[string]string `yaml:"env,omitempty"` // Environment variables baked into the runtime image
Package string `yaml:"package"` // e.g., "@upstash/context7-mcp"
Version string `yaml:"version,omitempty"` // e.g., "1.0.14"
Args []string `yaml:"args,omitempty"` // Additional arguments for the package
Env map[string]string `yaml:"env,omitempty"` // Environment variables baked into the runtime image
BuildWith []string `yaml:"build_with,omitempty"` // Build-time dependency constraints (uvx:// only), e.g. "mcp<2"
}

// MCPServerProvenance contains supply chain provenance information
Expand Down Expand Up @@ -370,10 +371,14 @@ func generateDockerfile(ctx context.Context, spec *MCPServerSpec, customTag stri
// Create image manager
imageManager := images.NewImageManager(ctx)

// Pass runtime env vars through as a RuntimeConfig override, if declared
// Pass runtime env vars and build-time dependency constraints through as a
// RuntimeConfig override, if declared
var runtimeOverride *templates.RuntimeConfig
if len(spec.Spec.Env) > 0 {
runtimeOverride = &templates.RuntimeConfig{RuntimeEnv: spec.Spec.Env}
if len(spec.Spec.Env) > 0 || len(spec.Spec.BuildWith) > 0 {
runtimeOverride = &templates.RuntimeConfig{
RuntimeEnv: spec.Spec.Env,
BuildWith: spec.Spec.BuildWith,
}
}

// Generate Dockerfile using toolhive's BuildFromProtocolSchemeWithName function with dryRun=true
Expand Down
27 changes: 27 additions & 0 deletions docs/adding-servers.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ spec:
- "arg2"
env: # Optional: env vars baked into the runtime image
SOME_VAR: "some-value" # Present in the running container, not just at build time
build_with: # Optional, uvx only: PEP 508 constraints on transitive deps
- "some-transitive-dep<2" # Passed to `uv tool install --with`; see uvx section below

provenance: # Optional but recommended
repository_uri: "https://github.com/user/repo"
Expand Down Expand Up @@ -121,6 +123,30 @@ provenance:
repository_ref: "refs/tags/v1.5.2"
```

Some packages leave a transitive dependency unbounded upstream, which can break
the build the moment that dependency ships a breaking release. Constrain it
with `build_with` rather than waiting on an upstream fix:

```yaml
# adb-mysql-mcp-server depends on mcp[cli]>=1.8.0 with no upper bound; mcp 2.0.0
# removed the module this server imports at startup, so pin it below 2 until
# upstream caps the dependency themselves.
spec:
package: "adb-mysql-mcp-server"
version: "2.0.0"
build_with:
- "mcp<2"
# Results in: uv tool install --with 'mcp<2' adb-mysql-mcp-server==2.0.0
```

`build_with` entries are PEP 508 requirement specifiers (exact pin, version
cap, or any other valid specifier) and only apply to `uvx://` builds; setting
them on an `npx` or `go` spec fails the build with an explicit error instead
of silently ignoring the constraint. The same values also apply to the
security scan (`scripts/mcp-scan`), which invokes the package directly via
`uvx --with ... package@version` so scanning matches what actually ships in
the built image.

### Go

```yaml
Expand Down Expand Up @@ -321,6 +347,7 @@ go build -o build/dockhand ./cmd/dockhand
| 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 |
| `ModuleNotFoundError` from an unpinned transitive dep | Add a `build_with` constraint (uvx only), e.g. `mcp<2` |

## Key Rules

Expand Down
8 changes: 8 additions & 0 deletions scripts/mcp-scan/generate_mcp_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,17 @@ def main():
spec_args = data['spec'].get('args', [])
spec_args_str = ' '.join(spec_args) if spec_args else ''

# Build-time dependency constraints (uvx only, matches dockhand/ToolHive's
# --build-with). Passed to uvx's own --with flag so the scanner invokes
# the same constrained dependency set as the built container image.
build_with = data['spec'].get('build_with', [])
build_with_str = ' '.join(f"--with {c}" for c in build_with) if build_with else ''

if protocol in ['npx', 'uvx']:
command = protocol
args = f"{package}@{version}"
if protocol == 'uvx' and build_with_str:
args = f"{build_with_str} {args}"
if spec_args_str:
args = f"{args} {spec_args_str}"
elif protocol == 'go':
Expand Down
8 changes: 7 additions & 1 deletion scripts/mcp-scan/run_scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,13 @@ def main():
if command == "npx":
scanner_args.append("--stdio-arg=--yes")
for arg in package_arg.split():
scanner_args.extend(["--stdio-arg", arg])
# Args that themselves start with "-" (e.g. uvx's "--with") are
# indistinguishable from a new flag to argparse when passed as a
# separate token after "--stdio-arg", so fold them into one token.
if arg.startswith("-"):
scanner_args.append(f"--stdio-arg={arg}")
else:
scanner_args.extend(["--stdio-arg", arg])

# Add mock environment variables for servers that require them
# mcp-scanner supports --stdio-env KEY=VALUE (can be repeated)
Expand Down
6 changes: 6 additions & 0 deletions uvx/adb-mysql-mcp-server/spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ metadata:
spec:
package: "adb-mysql-mcp-server"
version: "2.0.0"
# adb-mysql-mcp-server depends on `mcp[cli]>=1.8.0` with no upper bound.
# mcp 2.0.0 removed the mcp.server.fastmcp module this server imports at
# startup, so cap the transitive `mcp` dependency until upstream pins it
# themselves. See https://github.com/aliyun/alibabacloud-adb-mysql-mcp-server
build_with:
- "mcp<2"

provenance:
repository_uri: "https://github.com/aliyun/alibabacloud-adb-mysql-mcp-server"
Expand Down
Loading