skillscript-runtime v0.4.1
RemoteMcpConnector + per-connector tool allowlist + YouTrack proving
end-to-end. First "external-MCP-in-Skillscript" release. v0.4.0
shipped the config-plumbing mechanism (loader, validation, lint,
discovery, discipline); v0.4.1 ships the first JSON-instantiable
connector class and proves it works against real YouTrack through the
mcp-remote bridge.
Spec: Perry kickoff c65e77af + amendments 8a7356dc (allowlist) +
89e2752d (Scott's framing/scope answers).
Added
-
RemoteMcpConnectorclass (src/connectors/mcp-remote.ts). Child-
process spawn + JSON-RPC stdio bridge. Lifecycle: initialize
handshake,tools/calldispatch, clean shutdown viashutdownrequest
→ SIGTERM → SIGKILL fallback. No auto-restart in v0.4.1 — child crash
puts the connector into an error state; subsequent dispatch throws
RemoteMcpDispatchError. Library-levelconnector.call()returns the
raw MCP{content, isError}envelope; runtime'sunwrapToolResult
does the convention-aware unwrap (text →JSON.parse). -
Closed-set class registry adds
RemoteMcpConnectorwith
fromConfigfactory. Existing v0.4.0connectors.jsonshapes pointing
at this class now instantiate cleanly. -
framingconfig option —"lsp"(default) or"newline". The
YouTrack proving case exposed thatmcp-remotespeaks newline-
delimited JSON-RPC, NOT LSPContent-Lengthheaders. The
config-driven option (item 5 of the kickoff) is exactly the escape
hatch needed. Lint rejects unknown framing values. -
Per-connector
allowed_toolsallowlist. Optional config field on
connectors.json entries. Semantics:undefined= allow-all
(backward-compat with v0.4.0);[]= allow-none (staging disable
pattern); listed = exactly these tools, nothing else.- New tier-1 lint rule
disallowed-toolfires at compile time on
$ name.toolwheretoolisn't in the allowlist - Runtime defense-in-depth: dispatcher refuses disallowed calls even
if lint is bypassed (the "fail closed even when lint missed it"
backstop per8a7356dc) runtime_capabilitiessurfaces effective allowlist per connector
(ornullfor allow-all)
- New tier-1 lint rule
-
Env-block-as-scope
${VAR}substitution. v0.4.0 only resolved
${VAR}fromprocess.env. v0.4.1 resolves the connector'senv
block first, then merges into the substitution scope for the rest of
the config. Composable shape from Claude Desktop'smcp.json:"args": [..., "--header", "Authorization:${AUTH_HEADER}"], "env": { "AUTH_HEADER": "Bearer ${YOUTRACK_TOKEN}" }
YOUTRACK_TOKENresolves from process env;AUTH_HEADERderives once
in the env block; args reference the derived value. -
Loader-time gitignore-detection warning (folded v0.4.0 deferral).
At startup, ifconnectors.jsonis detected in a.git-tracked
directory without a.gitignoreentry covering it, emit a one-time
stderr warning. Informational; doesn't block startup. -
unknown-connectorlint auto-wires from runtime registry (folded
v0.4.0 deferral, closes Perry's signoff observationa4ae08a6). MCP
lint_skill+compile_skillhandlers auto-pass the running
runtime'sregistryto lint, sounknown-connector+disallowed- toolfire by default without callers having to remember explicit
options. Cold-author callers targeting a different runtime can
override via explicitmcpConnectorNames/mcpConnectorAllowedTools. -
foreachover parsed-JSON arrays (folded v0.4.0 deferral). v0.2.5
extendedin-RHS to tolerate JSON-string values that parse to arrays;
v0.4.1 mirrors that tolerance intoforeach. Lets
foreach I in $(RAW):work whenRAWis a string that JSON-parses
to an array (e.g., from a~op response).$ json_parse-bound
arrays already worked since the parsed structure is in the vars map
directly. -
Kwarg type coercion in
$op calls. Pre-v0.4.1,limit=5in
$ youtrack.search_issues query="for: me" limit=5was sent as the
string"5". YouTrack (and other typed MCPs) rejected with "expected
integer, got String." v0.4.1 addscoerceKwargValuein
parseToolArgs: unquoted^-?\d+$→ integer,^-?\d+\.\d+$→
number,true/false→ boolean,null→ null,
[...]/{...}shapes → JSON-parsed if valid. Quoted strings force
the string type (count="5"stays "5"). -
YouTrack proving end-to-end (
tests/v0.4.1-youtrack-proving.test.ts).
8 tests covering direct connector dispatch (initialize, tools/list,
get_current_user,search_issueswith integer kwarg), full skill
chain (examples/youtrack-morning-sweep.skill.mdcompile + execute),
kwarg type coercion regression-lock, allowlist enforcement (positive +
negative against real YouTrack). Always-fail-if-YOUTRACK_TEST_TOKEN-
missing per Scott's call (89e2752d). CI workflow updated.
Example skill
examples/youtrack-morning-sweep.skill.md checked in — the canonical
"external remote MCP in Skillscript" proving case. Compiles + executes
against real YouTrack given a configured youtrack connector.
Implementation notes
-
Narrow-core LOC ceiling 6000 → 6600. ~330 LOC for the
mcp-remote.tsconnector class (spawn + framing + lifecycle), ~60
LOC across Registry + config + lint + runtime for allowlist
plumbing, ~40 LOC for env-block-as-scope substitution refactor, ~50
LOC for kwarg coercion + foreach JSON tolerance + lint auto-wire
threading. ~50 LOC for the gitignore-detection helper. New file
takes us to 15 narrow-core files; ceiling stays under 20. -
Tests: 50 new across
tests/v0.4.1-mcp-remote.test.ts(21 —
bridge core via mock child processes),tests/v0.4.1-allowlist.test.ts
(17 — allowed_tools loader + lint + runtime + discovery),
tests/v0.4.1-folded.test.ts(12 — gitignore detection, lint auto-
wire, foreach over parsed-JSON), plus 8 in
tests/v0.4.1-youtrack-proving.test.ts(live YouTrack chain). Total
58 new; 955 passing in the suite (3 long-skip browser dogfood). -
CI requires
YOUTRACK_TEST_TOKENsecret. Set in repo settings.
Token should be allowlist-scoped (read-only YouTrack tools), not a
personal admin token. Failure-mode is loud: missing token → CI fails
at the test step → no publish (avoids silent regression).