build/skills/charly-mcp-cmd/SKILL.md
MUST be invoked before any work involving: Model Context Protocol — both directions. (1) the declarative `mcp:` check verb (the MCP CLIENT): probing MCP servers declared via mcp_provide, testing MCP tool catalogs, the host-side URL-rewriter (including host-networked containers via `HostConfig.NetworkMode` detection) or port-publishing behavior — served OUT-OF-PROCESS by `candy/plugin-mcp` (there is NO host `charly check` subcommand for it). (2) `charly mcp serve` server: running the charly CLI itself as an MCP server over Streamable HTTP or stdio, auto-generated from Kong reflection (~192 tools including the MCP-first authoring surface — image/layer scaffolding, comment-preserving YAML edits, free-form file writes), destructive-hint annotations, the `--read-only` filter, auto-fallback to `overthinkos/overthink` when cwd has no `charly.yml` (always fires regardless of CHARLY_PROJECT_DIR being set), and the `charly-mcp` deployment layer with its `/workspace` bind mount. Named `charly-mcp-cmd` (not `mcp`) to disambiguate from Claude Code's built-in `/mcp` slash command (the `-cmd` suffix avoids collision with the existing `/charly-coder:charly-mcp` image skill).
npx skillsauth add overthinkos/overthink-plugins charly-mcp-cmdInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
charly speaks MCP in both directions. This skill covers both:
mcp: check verb: connect to any MCP server declared via mcp_provide and probe/call/read. Authored as an mcp: step in a candy/box plan and run via charly check live <image> --filter mcp; it is served OUT-OF-PROCESS by candy/plugin-mcp and has no host charly check subcommand (parallel to the kube:/spice:/adb:/appium: plugin verbs). Used to test MCP endpoints shipped by jupyter-mcp, chrome-devtools-mcp, or the charly server itself.charly mcp serve: expose the entire charly CLI surface (build + test + deploy modes, 190 tools) as MCP over Streamable HTTP or stdio. Used by LLM agents (Claude Code, Open WebUI, OpenClaw) to drive charly remotely. Deployed in-container via the charly-mcp layer. The 190-tool catalog now includes a project-scaffolding + YAML-editing + file-write authoring surface, so an agent can build an charly project from scratch over RPC — see "Authoring tools" below.Both surfaces share the same SDK: github.com/modelcontextprotocol/go-sdk v1.5.0 — the client half now lives in candy/plugin-mcp (out of charly's core); charly's core keeps the SDK only for the charly mcp serve server (charly/mcp_server.go).
mcp: check verb)The mcp: check verb connects to Model Context Protocol servers declared by running containers via mcp_provide, using github.com/modelcontextprotocol/go-sdk (v1.5.0). Seven methods cover the full MCP client surface: ping, servers, list-tools, list-resources, list-prompts, call, read. No MCP URL argument is ever typed by the user — the HOST reads the target image's ai.opencharly.mcp_provide OCI label, resolves {{.ContainerName}} templates, applies pod-aware localhost rewriting, and maps the container-network URL to the published host port automatically (preresolveMcpEndpoint), then hands the pre-resolved endpoint to the plugin.
Served out-of-process — no host CLI subcommand. The verb is a DECLARATIVE check verb only; there is no charly check subcommand for it (just like kube:/spice:/adb:/appium:). The MCP-client implementation (the go-sdk dial + the 7 methods) lives in candy/plugin-mcp, an out-of-tree charly plugin that charly's loader go-builds on the host and serves out-of-process over go-plugin gRPC (LocalTransport). A check: step carrying mcp: dispatches through the provider registry exactly like a built-in verb (ResolveVerb("mcp") → grpcProvider → Provider.Invoke with the full Op marshaled + a CheckEnv snapshot). Exercise it with charly check live <image> --filter mcp.
Each mcp: step is a check: step node in a candy/box plan. The method name becomes the verb's YAML value; method-specific args are sibling fields (tool:, uri:, input:, mcp_name:). A probe is a check: step node — a top-level, name-first node (there is no plan: list key). Shared matchers (stdout:, stderr:, exit_status:, timeout:) work like other verbs. See /charly-check:check for the parent router and the complete method allowlist. Example:
jupyter-mcp-list-tools:
check: jupyter exposes its core notebook mcp tools
mcp: list-tools
context: [deploy]
stdout:
- contains: insert_cell
- contains: execute_cell
| Action | Declarative step | Description |
|--------|------------------|-------------|
| Ping | mcp: ping | Liveness check — emits ok on a successful Ping RPC |
| Enumerate | mcp: servers | List MCP servers declared by the image (no dial) |
| List tools | mcp: list-tools | Tool name + first-line description per line |
| List resources | mcp: list-resources | URI + name + MIME type per line |
| List prompts | mcp: list-prompts | Prompt name + description per line |
| Call | mcp: call + tool: (+ optional input:) | Invoke a tool; emits TextContent payload |
| Read | mcp: read + uri: | Read a resource; emits Text content |
Every method accepts:
mcp_name: <server> — disambiguate when the image declares multiple mcp_provide entriestimeout: <duration> — per-operation timeout (default 30s)stdout:, stderr:, exit_status:)Output is always tab-separated plaintext fed to the matcher pipeline (no --json form — that was a property of the retired host CLI). Run a candy's baked mcp: steps against a live deployment with charly check live <image> --filter mcp.
The HOST owns all podman / OCI-label / port-mapping resolution and pre-resolves a single host-routable dial endpoint (preresolveMcpEndpoint) into the check env; the out-of-process candy/plugin-mcp provider then builds a single *mcp.ClientSession against that endpoint per invocation. The plugin needs no container inspection at all.
resolveContainer(image, instance) → charly-<image>[-<instance>].containerImageRef → ExtractMetadata → meta.MCPProvide (read from OCI label ai.opencharly.mcp_provide).{{.ContainerName}} in the URL is replaced with the resolved container name.podAwareMCPProvides folds same-image entries so the URL host becomes localhost (identical to the CHARLY_MCP_SERVERS path used by charly config).localhost, it looks up the published host port for the URL's port via podman inspect (same NetworkSettings.Ports data that powers ${HOST_PORT:N} in declarative tests) and rewrites to 127.0.0.1:<host-port>. Non-matching hosts (external URLs) pass through unchanged. The pre-resolved mcp_provides + the single picked endpoint travel to the plugin in the CheckEnv snapshot.transport: http (or empty) → StreamableClientTransport{Endpoint}; transport: sse → SSEClientTransport{Endpoint}; any other string errors at dial time with the declared transport echoed.mcp.NewClient(…).Connect(ctx, transport, nil) runs the MCP initialize handshake and returns an opened ClientSession; the provider evaluates the step's matchers itself and returns a {status,message} verdict to the host. A defer session.Close() always fires.Source: the host pre-resolver charly/mcp_preresolve.go (preresolveMcpEndpoint); the MCP client (the 7 methods + the dial) lives in candy/plugin-mcp (provider.go dispatch + methods.go client layer). See /charly-internals:plugin for the out-of-process provider model and /charly-internals:go for the host-side map.
Each method is shown as the declarative mcp: step you author. Run them against a live deployment with charly check live <image> --filter mcp.
jupyter-mcp-ping:
check: the jupyter mcp server responds to ping
mcp: ping
context: [deploy]
stdout:
equals: ok
Calls ClientSession.Ping(ctx, nil). Passes iff the server responds. Useful as a liveness check, often paired with a short timeout:.
jupyter-mcp-servers:
check: the image advertises the jupyter mcp server
mcp: servers
context: [deploy]
stdout:
contains: jupyter # emits "<name>\t<url>\t<transport>" per server, e.g. jupyter http://localhost:8888/mcp http
Reads mcp_provide from the OCI label, applies template substitution + pod-aware rewrite, emits the result. No MCP handshake — metadata only. Useful for confirming which server names the image advertises before dialing.
jupyter-mcp-list-tools:
check: the jupyter mcp server exposes its notebook tools
mcp: list-tools
context: [deploy]
stdout:
- contains: list_notebooks # "list_notebooks\tList all notebooks accessible in the workspace."
- contains: execute_cell
Plaintext output is tab-separated (name\tfirst-line-of-description) for easy contains: matching without JSON parsing. Multi-line descriptions collapse to the first non-empty line. list-resources emits uri\tname\tmime; list-prompts emits name\tdescription. The provider automatically pages through NextCursor so all results return in a single invocation.
jupyter-mcp-call-list-notebooks:
check: list_notebooks returns successfully
mcp: call
context: [deploy]
tool: list_notebooks # required
input: "{}" # optional JSON arg blob; omit for zero-arg tools
exit_status: 0 # assert no IsError
The input: field is the tool's arguments as a JSON object (optional — omit for zero-arg tools), parsed with encoding/json. Returned TextContent blocks emit one per line; ImageContent / AudioContent emit a [image content: <mime>, <N> bytes] placeholder. If the server sets IsError: true, the step FAILS with the error text.
mcp-read-resource:
check: reading a resource returns a non-empty body
mcp: read
context: [deploy]
uri: file:///workspace/data.txt # required
stdout:
- matches: "." # non-empty body
Calls ReadResource(URI: …). Text content is emitted to stdout; binary blobs emit a [binary resource …] placeholder.
When an image declares more than one mcp_provide entry, every mcp: step requires the mcp_name: modifier; without it the step FAILS with image provides multiple mcp servers; use mcp_name (available: jupyter, chrome-devtools):
chrome-devtools-mcp-ping:
check: the chrome-devtools mcp server responds to ping
mcp: ping
mcp_name: chrome-devtools
context: [deploy]
No existing image ships multiple providers today — jupyter layers expose one server (jupyter), chrome-devtools-mcp exposes one (chrome-devtools). The disambiguation exists for future compositions.
Symptom: a mcp: ping step (e.g. via charly check live <image> --filter mcp) fails with:
mcp chrome-devtools: container port 9224/tcp is not published to a host port;
declare `ports: [9224:9224]` in the image or run the test from inside the pod
Cause: the image's OCI label declares the port (e.g., chrome-devtools-mcp adds 9224 to sway-browser-vnc's port list), but the running container's quadlet doesn't publish it because charly.yml's per-image port: entry replaces (not appends to) the image-declared list. When a new mcp-providing layer is added to an image that already has a charly.yml entry with an explicit port: list, the new port silently drops out.
Fix: either add the mcp port to the instance's charly.yml entry:
# ~/.config/charly/charly.yml
sway-browser-vnc:
pod:
image: sway-browser-vnc
sway-browser-vnc-port:
port:
- 5900:5900
- 9250:9222
- 9224:9224 # ← add this
… or remove the port: override entirely so the image default (which already includes 9224) applies. Re-run charly config <image> and restart the service (charly stop && charly start — charly update may no-op if the image tag hasn't changed).
Verification that the fix worked:
charly status <image> --json
# Expect: the mcp port listed in the port mappings with a non-null host port
charly check live <image> --filter mcp
# the mcp: ping step passes
Instance-specific deployments (the -i <instance> form) typically have their ports declared at create-time and are less prone to this drift.
| Declared transport: | SDK transport | Notes |
|-----------------------|---------------|-------|
| http or empty | StreamableClientTransport{Endpoint} | Project default; jupyter-mcp and chrome-devtools-mcp both speak this |
| streamable, streamable-http | StreamableClientTransport{Endpoint} | Aliases |
| sse | SSEClientTransport{Endpoint} | Legacy SSE servers |
| anything else | error | "unsupported mcp transport %q (expected http or sse)" |
The SDK's transports are struct-literal constructors — there is no NewStreamableClientTransport(...) helper. Setting Endpoint is the only required field.
Every method emits author-friendly plaintext with one record per line:
ping → oklist-tools → <name>\t<first-line-of-description>list-resources → <uri>\t<name>\t<mime-type>list-prompts → <name>\t<description>call → concatenated TextContent.Text payloads, one per lineread → concatenated ResourceContents.Text, one per lineservers → <name>\t<url>\t<transport>The plaintext is fed straight to the matcher pipeline. There is no JSON output form — the retired host CLI's --json flag went away with the subcommand; the out-of-process candy/plugin-mcp provider returns the plaintext to the host, which evaluates the step's matchers.
Checks currently shipping in the three provider candies (candy/jupyter/charly.yml, candy/jupyter-ml/charly.yml, candy/chrome-devtools-mcp/charly.yml), in each candy's plan (as top-level check: step nodes):
# Liveness check — fastest sanity verification
jupyter-mcp-ping:
check: the jupyter mcp server responds to ping
mcp: ping
context: [deploy]
timeout: 10s
# Catalog assertion — ensure the server exposes the tools we expect
jupyter-mcp-list-tools:
check: the jupyter mcp server exposes the expected tools
mcp: list-tools
context: [deploy]
stdout:
- contains: insert_cell
- contains: execute_cell
# Real tool invocation — exercises the full request/response path
jupyter-mcp-call-list-notebooks:
check: list_notebooks returns successfully
mcp: call
context: [deploy]
tool: list_notebooks
input: "{}"
exit_status: 0
# Tool with arguments
jupyter-mcp-call-get-notebook:
check: get_notebook returns a notebook with cells
mcp: call
context: [deploy]
tool: get_notebook
input: '{"path":"getting-started.ipynb"}'
stdout:
- contains: cells
# Resource read
jupyter-mcp-read-prompt:
check: a prompt resource reads back non-empty
mcp: read
context: [deploy]
uri: file:///workspace/prompt.txt
stdout:
- matches: "."
Each mcp: step is a check: step — a deterministic probe that satisfies the mandatory-ADE gate. Deploy-context only. mcp: steps require a running container with the mcp port published; charly box validate rejects build-context mcp steps at authoring time, and charly check box skips them at runtime with the message "mcp: <method> requires a running container (skip under charly check box)". Follow the same rule as the other four live-container verbs — cdp, wl, dbus, vnc.
charly box validate + the CUE schema enforce (the mcp verb is an out-of-process plugin — its method-name enum is validated by CUE on core #Op, its required-modifier checks run in candy/plugin-mcp at dispatch):
mcpMethods (7 entries); unknown methods list the allowed set in the error.context must include deploy; a build-context mcp step raises "mcp: verb requires context:\"deploy\"".call requires tool:; missing field raises "mcp: call requires modifier \"tool\"".read requires uri:.No other required modifiers — ping, servers, list-* take only the optional mcp_name:.
charly mcp serve)charly mcp serve runs the charly CLI as an MCP server. Every leaf command in the Kong CLI tree — box.build, status, test.mcp.ping, config.setup, box.new.project, candy.add-rpm, etc. — becomes a callable MCP tool. Tool catalogs are auto-generated from Kong struct tags by reflection; there is no hand-written schema per command. Result: 190 tools covering the entire build + test + deploy surface, including the MCP-first authoring verbs (image.{new.project, new.image, set, add-layer, rm-layer, fetch, refresh, write, cat} + layer.{set, add-rpm, add-deb, add-pac, add-aur}).
charly mcp serve # Streamable HTTP on :18765/mcp
charly mcp serve --listen 127.0.0.1:9999 # Custom port
charly mcp serve --path /api/mcp # Custom HTTP path prefix (default /mcp)
charly mcp serve --stdio # Stdio transport for editor/LLM integration
charly mcp serve --read-only # Skip registering the 51 destructive tools
The server uses the same github.com/modelcontextprotocol/go-sdk v1.5.0 as the client (the candy/plugin-mcp provider), so the wire format is identical and the mcp: ping check verb works against it unchanged.
The server lives in a single file: charly/mcp_server.go.
Reflection — buildMcpServer(readOnly) calls kong.New(&CLI{}, …) to materialise the CLI tree, then walks k.Model.Leaves(true) — every non-branch node. For each leaf, kongLeafToTool(leaf, path, destructive) emits an *mcp.Tool:
box.build, test.mcp.ping, config.setup).Help tag, with a [destructive: …] annotation appended for mutating tools.long:"", help:"", enum:"", default:"", required:"" struct tags. Positional args become required properties; flags become optional properties. Every schema has additionalProperties: false — unknown keys are rejected by the SDK's input validation before the handler runs. The schema validator is LLM-honest about the allowed surface.DestructiveHint: &true; everything else gets ReadOnlyHint: true.Destructive gating — mcpDestructivePaths is an explicit 63-entry allowlist of mutating tools (lifecycle: remove/stop/start/update/cmd/shell/service.*; config: config.setup/mount/unmount/passwd/remove; secrets: set/delete/import/init/gpg.setup/gpg.set/gpg.unset/gpg.edit/gpg.encrypt/gpg.add-recipient/gpg.import-key; deploy: import/reset; image build/scaffold/edit: box.build/merge/new.{layer,project,image}/set/add-layer/rm-layer/refresh/write; layer edit: candy.set/add-rpm/add-deb/add-pac/add-aur; VM: create/destroy/start/stop/build; udev: install/remove; alias: install/uninstall/add/remove; record: start/stop/cmd; tmux: kill/run/send/cmd; settings: set/reset/migrate-secrets). When --read-only is set, buildMcpServer skips registration entirely rather than gating at runtime — read-only servers expose 127 tools (190 − 63). box.fetch (idempotent, additive cache prime) and box.cat (read-only file read) are not in the destructive set despite living in the authoring family — they're safe under --read-only.
Tool invocation — makeToolHandler(path, leaf) returns a closure. On call: decode the MCP JSON arguments, reconstruct a []string argv via argvFromJSON(…) (booleans → bare flag, slices → repeated --flag=value, positionals in Kong order), then captureAndRun(argv) builds a fresh kong.New(&CLI{}), calls k.Parse(argv), invokes kctx.Run(), and returns captured stdout/stderr as a single TextContent. Errors become IsError: true tool results, not MCP-protocol errors — the LLM sees the failure text.
Capture model — os.Stdout / os.Stderr redirect, NOT fd-level — capture redirects the os.Stdout/os.Stderr package variables only, not fd 1/2 via syscall.Dup2. Fd-level capture deadlocks: the MCP SDK's StdioTransport captures os.Stdout by pointer at Connect time, so dup2'ing fd 1 to a capture pipe sends the SDK's JSON-RPC responses into the tool-output buffer instead of to the client. Because the redirect is at the package-variable level, every charly command must write via fmt.Println/fmt.Printf (not Go's builtin println, which bypasses os.Stderr and writes directly to fd 2) — VersionCmd uses fmt.Println for exactly this reason. Subprocess output is a known leak — but the charly codebase invokes subprocesses via cmd.Stdout = os.Stderr etc., so in practice it stays captured.
Serialisation — runMu (sync.Mutex) serialises handler invocations because os.Stdout/os.Stderr redirects are global. MCP tool calls on the server run sequentially; most tools are I/O-bound shell-outs to podman so parallelism is not a big loss.
Transport — mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server { return server }, nil) wraps the server for HTTP mode. In stdio mode, server.Run(ctx, &mcp.StdioTransport{}) blocks on stdin.
charly-mcp layerComposing charly-mcp into an image deploys the server via supervisord:
# candy/charly-mcp/charly.yml (summary; the candy's required version: is elided)
charly-mcp:
candy:
description: Runs the charly CLI itself as an MCP server (charly mcp serve) over Streamable HTTP on :18765
charly-mcp-candy:
candy:
- charly
- supervisord
charly-mcp-port:
port:
- 18765
charly-mcp-mcp_provide:
mcp_provide:
- name: charly
url: "http://{{.ContainerName}}:18765/mcp"
transport: http
charly-mcp-volumes:
volumes:
- name: project # Bind-mount the project root; see below
path: /workspace
charly-mcp-env:
env:
CHARLY_PROJECT_DIR: "/workspace"
charly-mcp-service:
service:
- name: charly-mcp
exec: /usr/local/bin/charly mcp serve --listen :18765
restart: always
enable: true
scope: system
Project-dir wiring — build-mode tools (box.build, box.inspect, box.list.*) resolve charly.yml via os.Getwd(). Inside the container, cwd is /workspace (set by the charly-mcp layer's CHARLY_PROJECT_DIR env + volume: declaration). Three deployment patterns, in order of progressively less local setup:
Bind-mount — the canonical charly-mcp pattern. The candy ships env: CHARLY_PROJECT_DIR: /workspace + volumes: project → /workspace; the deployer attaches a host checkout via charly config <image> --bind project=/path/to/opencharly. The charly CLI's global -C / --dir / CHARLY_PROJECT_DIR flag honours the env var before Kong dispatch, calling os.Chdir(CHARLY_PROJECT_DIR) once. Use this when the agent should see your in-flight local edits. The volume NAME is project (stable bind-mount API); the in-container PATH is /workspace (the generic name works whether the contents are an opencharly checkout or any other workspace).
Remote pin — set CHARLY_PROJECT_REPO=overthinkos/overthink@<sha-or-ref> in the container env (e.g. via charly config <image> -e CHARLY_PROJECT_REPO=...). The charly CLI clones (or hits its ~/.cache/charly/repos/ cache) and chdirs into the cache path before Kong dispatch. No bind mount required. Use this for reproducible agent runs against a pinned upstream.
Auto-default — charly mcp serve with no charly.yml reachable at cwd silently falls back to github.com/overthinkos/overthink. The fallback fires regardless of CHARLY_PROJECT_DIR being set — it checks whether the resolved cwd actually contains charly.yml, not whether the env var is populated. This matters because the charly-mcp layer permanently sets CHARLY_PROJECT_DIR=/workspace: a deployer who forgets the --bind still gets a working MCP server backed by the upstream repo, with a log line naming the reason (charly mcp: CHARLY_PROJECT_DIR=/workspace has no charly.yml; falling back to default repo …). Pass --no-default-repo on the serve command to opt out (hard-fail with a clear message). This is the only command in the entire CLI that auto-fetches; the top-level CLI stays opt-in. Implementation: McpServeCmd.bootstrapProject() in charly/mcp_server.go.
See /charly-image:image "Project directory resolution" for the flag/env semantics, and charly/mcp_serve_default_repo_test.go for the auto-fallback behaviour test.
Composition style — charly-mcp uses candy: [charly, supervisord] (meta-layer composition) rather than require: (hard prerequisite) because it adds no install of its own — it's pure wiring. Boxes that want the MCP server add charly-mcp to their candy list; boxes that just want the charly binary continue to use the charly candy alone. Both candy: and require: reference other candies, but only candy: lets the using candy ship no install files.
# Build an charly-mcp-bearing image (e.g. charly-arch) and start it:
charly box build charly-arch
charly config charly-arch --bind project=/home/you/opencharly
charly start charly-arch
# From the host — the container's mcp_provide URL is auto-rewritten to the published host
# port, then the candy's baked declarative mcp: steps (each carrying mcp_name: charly)
# run against the live deployment:
charly check live charly-arch --filter mcp
# the mcp: ping / list-tools / call (version, box.list.boxes) steps all pass
# list-tools → 190 tools; call version → 2026.nnn.nnnn; call box.list.boxes →
# charly-arch [testing], arch [testing], … (reads charly.yml from the bind-mounted /workspace)
The deploy-context check: step nodes in candy/charly-mcp/charly.yml cover this exact sequence: service-running, port-reachable, mcp: ping, mcp: list-tools, mcp: call tool=version, mcp: call tool=box.list.boxes (the last proves the bind-mount + CHARLY_PROJECT_DIR wiring). For an ad-hoc MCP client outside charly, point any MCP-speaking tool (e.g. /charly-tools:mcporter) at the published host port.
Every CLI verb under charly box … and charly candy … auto-becomes an MCP tool via Kong reflection. The authoring surface added for "build a project from scratch using only charly mcp" exposes these tools:
| MCP tool | What it does |
|---|---|
| box.new.project | Scaffold charly.yml (the build vocabulary is embedded in the charly binary), candy/, .gitignore. |
| box.new.box | Append a new image entry to charly.yml. |
| box.new.candy | Scaffold candy/<name>/charly.yml with a stub. |
| box.set | Set any value in charly.yml by dot-path (defaults.tag, images.foo.layers, …). Value is parsed as YAML. |
| box.add-candy / box.rm-candy | Append / remove a layer from an image's candy: list (idempotent). |
| candy.set | Set any value in candy/<name>/charly.yml by dot-path. |
| candy.add-rpm / candy.add-deb / candy.add-pac / candy.add-aur | Append packages to a layer's <format>.packages list. Idempotent. Upgrades scaffold's null package: value to a real sequence. |
| box.fetch / box.refresh | Pre-prime / re-clone the remote-repo cache. Spec defaults to default (overthinkos/overthink). |
| box.write / box.cat | Write / read any file under the project root — escape hatch for free-form auxiliary files (pixi.toml, package.json, root.yml, scripts, *.service). Path is resolved against os.Getwd() and rejected if it escapes the project root. |
All YAML edits go through the yaml.v3 node API (not value unmarshal) so comments and key order are preserved across edits. Implementation in charly/scaffold_project.go, charly/yaml_setter.go, and charly/scaffold_cmds.go. Tested in charly/scaffold_project_test.go and charly/yaml_setter_test.go.
End-to-end MCP-only worked example:
// All called as MCP tool calls (e.g. via an `mcp: call` check step, or any MCP client):
box.new.project {"dir": "/tmp/hello"}
box.new.box {"name": "hello", "base": "quay.io/fedora/fedora:43"}
box.new.candy {"name": "hello-svc"}
candy.add-rpm {"name": "hello-svc", "packages": ["openssh-server"]}
box.add-candy {"image": "hello", "layer": "hello-svc"}
image.validate {}
box.build {"image": "hello"}
box.inspect {"image": "hello"}
Default :18765 chosen for non-collision with other MCP layers:
8888 — jupyter-mcp9224 — chrome-devtools-mcp (via mcp-proxy)18789 — openclaw gatewayThe server registers destructive tools with DestructiveHint: true rather than withholding them. The LLM runtime (Claude Code, Open WebUI) is responsible for acting on the hint — e.g. prompting the user before calling an annotated tool. For hostile-LLM scenarios or untrusted network deployments, run with --read-only (drops the 51 mutating tools at registration time) and/or restrict reach via the tunnel / Traefik candy.
/charly-check:check — parent router for the declarative mcp: check verb. The full mcp method allowlist (and how the verb relates to the other out-of-process live-container plugin verbs wl/cdp/vnc/dbus/record/kube/spice/adb/appium/libvirt) lives in its "Live-container verb catalog" + "Method allowlist — mcp" sections./charly-image:layer — mcp_provide / mcp_accept / mcp_require field reference for layer authoring./charly-core:charly-config — how mcp_provide gets injected into charly.yml provides.mcp: and synthesized into CHARLY_MCP_SERVERS for consumers at charly config time; pod-aware resolution to localhost; instance-aware MCP server naming with -<instance> suffix./charly-build:validate — authoring-time validation rules (method allowlist, required modifiers, scope enforcement)./charly-core:deploy — charly.yml port: override semantics (the port-publishing gotcha lives here operationally)./charly-check:cdp — sibling live-container verb (Chrome DevTools Protocol)./charly-check:wl — sibling (Wayland desktop control)./charly-check:dbus — sibling (D-Bus calls/notifications)./charly-check:vnc — sibling (VNC framebuffer / input)./charly-jupyter:jupyter-mcp — the FastMCP server implementation layer (11 tools for notebook manipulation over CRDT: notebook_/cell_ + notebook_list_users + room_list; clients do not manage CRDT rooms — the server auto-attaches)./charly-selkies:chrome-devtools-mcp — the mcp-proxy wrapper around chrome-devtools-mcp (29 tools for browser automation)./charly-hermes:hermes — a consumer (mcp_accept: jupyter, chrome-devtools); use the mcp: check verb (charly check live <consumer> --filter mcp) to verify the services hermes discovers are actually alive./charly-openwebui:openwebui — another consumer (mcp_accept: jupyter, chrome-devtools)./charly-jupyter:jupyter, /charly-jupyter:jupyter-ml, /charly-jupyter:jupyter-ml-notebook — images bundling jupyter-mcp; charly check live <image> --filter mcp exercises the verb end-to-end./charly-selkies:sway-browser-vnc, /charly-selkies:selkies-labwc, /charly-selkies:selkies-labwc-nvidia — images bundling chrome-devtools-mcp (transitively via the chrome metalayer)./charly-internals:go — host-side implementation map: mcp_preresolve.go (preresolveMcpEndpoint — host podman/OCI-label/port resolution into the check env), mcp_server.go (server: Kong→MCP reflection, destructive-hint set, captureAndRun), validate_check.go (op-level deploy-scope enforcement; the mcp method-name + required-modifier checks live in candy/plugin-mcp + the CUE #Op enum). The MCP CLIENT (the 7 methods + the go-sdk dial) lives out-of-process in candy/plugin-mcp — see /charly-internals:plugin./charly-coder:charly-mcp — the deployment layer that wires charly mcp serve into an image via supervisord. Includes the /workspace bind-mount (volume NAME project) + CHARLY_PROJECT_DIR env var pattern for build-mode tools./charly-tools:charly — the underlying binary layer; required by charly-mcp./charly-image:image — "Project directory resolution" subsection documents the -C / --dir / CHARLY_PROJECT_DIR global flag that makes the server's project-dir bind-mount work./charly-coder:charly-arch, /charly-distros:charly-fedora — canonical images composing charly-mcp for remote charly-via-MCP deployments.MUST be invoked when the task involves Model Context Protocol on either side:
mcp: check verb (served out-of-process by candy/plugin-mcp; no host charly check subcommand), probing/testing MCP servers declared via mcp_provide, examining MCP tool/resource/prompt catalogs, debugging the URL-rewriter or port-publishing behavior, or authoring deploy-context mcp: step nodes in a candy/box plan.charly mcp serve operation, the charly-mcp layer, destructive-hint policy, the --read-only filter, Kong-reflection tool generation, the project-dir bind-mount pattern, or symptoms like "MCP tool returned empty output" (check println vs fmt.Println in the invoked command — the server captures os.Stdout, not fd 1).Invoke this skill BEFORE reading source code or launching Explore agents.
Workflow position: Test mode / live service operation. Client: deploy an image with mcp_provide, start it, then probe. Server: compose charly-mcp into an image, bind --bind project=/path/to/opencharly at config time, start the container, then consume from any MCP-speaking LLM runtime. Pairs with /charly-check:check (parent router + declarative-verb catalog), /charly-core:charly-config (consumer-side injection + --bind for the project dir), /charly-coder:charly-mcp (server deployment layer), and the consumer-layer skills (/charly-hermes:hermes, /charly-openwebui:openwebui).
tools
Use when authoring or modifying a charly PLUGIN — a candy with a `plugin:` block that contributes Providers (verbs/kinds/deploy-targets/steps/builders/commands), its own CUE schema, builtin (compiled-in) or external (out-of-tree git repo). Covers the unified Provider model, the per-plugin CUE-schema contract (single source → Go params for dev + schema-over-Describe RPC for runtime), the SDK, and the loader.
tools
The CUE data-validation / configuration CLI (cue), pinned to v0.16.1. Use when working with the cue candy, installing the cue binary into a box or onto a target:local dev host, or running the offline schema-vendoring pipeline that feeds charly's egress validation.
tools
CUE EGRESS validation — validating (and, where it adds value, generating) the config files charly WRITES to a system BEFORE the bytes hit disk. MUST be invoked before working on charly/egress.go, the vendored schemas under candy/plugin-egress/egress-schemas/vendor/, the ValidateEgress / registerVendoredEgressKind path, the offline `task cue:vendor` pipeline, or adding an egress schema for any written artifact (cloud-init, k8s manifests, traefik routes, runtime config, install ledger, systemd/quadlet units, ssh_config, libvirt XML).
tools
Kubernetes cluster-probe declarative check verb — the `kube:` check verb (nodes, pods, ingress, storage class, addon health, apply/delete, and arbitrary resource GETs) served out-of-process by the candy/plugin-kube plugin (vendored client-go; no external kubectl required).