Skip to content

Commit 2f2d61b

Browse files
Integrate native MCP servers with project grants and exact tool approvals
1 parent 911d9ee commit 2f2d61b

24 files changed

Lines changed: 2231 additions & 43 deletions

‎README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ runtime or browser launcher. See the [native development guide](docs/NATIVE_DESK
1616
and the [goals](docs/NATIVE_GOALS.md) and [model routing](docs/NATIVE_ROUTING.md)
1717
workflows, [managed background processes](docs/NATIVE_BACKGROUND.md), and
1818
[native slash commands and project skills](docs/NATIVE_WORKFLOWS.md),
19-
[reviewed lifecycle commands](docs/NATIVE_HOOKS.md), and a
19+
[reviewed lifecycle commands](docs/NATIVE_HOOKS.md),
20+
[approved MCP stdio tools](docs/NATIVE_MCP.md), and a
2021
[native CLI](docs/NATIVE_CLI.md) that shares the active desktop engine or runs
2122
headlessly with its own profile.
2223
Development AppImage and Debian

‎docs/NATIVE_CLI.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,26 @@ reads all settings, `config key` reads a dotted key, and `config key value`
119119
validates and saves JSON or text. Use the configured environment variable or
120120
desktop secret editor for credentials; never put secret values in shell history.
121121

122+
For [native MCP stdio integrations](NATIVE_MCP.md), register a JSON/YAML
123+
definition, inspect its command and hash, and explicitly enable it for the
124+
selected project:
125+
126+
```sh
127+
shadowcode mcp add /path/to/definition.json
128+
shadowcode --json mcp
129+
shadowcode mcp enable config:my-tools --hash HASH_FROM_CATALOG
130+
shadowcode mcp disable config:my-tools
131+
shadowcode mcp remove config:my-tools --hash HASH_FROM_CATALOG
132+
```
133+
134+
Registration is inert. `mcp add --hash CURRENT_HASH` replaces an existing global
135+
definition. Project files have IDs such as `project:.shadowcode/mcp/my-tools.yaml`.
136+
Each agent call still uses the same exact-argument approval flow as the window;
137+
noninteractive tasks stop for approval instead of accepting it automatically.
138+
Disabling affects new tasks; cancel running and queued tasks to end their
139+
existing grants. Native HTTP connections and `mcp serve/register` are still
140+
being migrated.
141+
122142
## One engine per profile
123143

124144
The AppImage runtime extracts each invocation into a private temporary directory.

‎docs/NATIVE_DESKTOP.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ Project [lifecycle commands](NATIVE_HOOKS.md) can be reviewed and enabled in
4747
Settings → Hooks or through the same native CLI. Their command gates and
4848
completion checks run inside the native task lifecycle without Python callbacks.
4949

50+
[Native MCP stdio servers](NATIVE_MCP.md) can be registered and enabled in
51+
Settings → MCP. The agent discovers their tools lazily and requests approval for
52+
each exact call. Connections close before the task finishes. HTTP and the
53+
ShadowCode MCP server remain part of the ongoing migration.
54+
5055
To work on the live interface, run `./ui/node_modules/.bin/tauri dev` from the
5156
repository root. The Tauri configuration starts Vite and builds the native app.
5257
Build hooks explicitly use the `ui` directory, including when the CLI is invoked

‎docs/NATIVE_MCP.md‎

Lines changed: 93 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,75 @@
11
# Native MCP migration — development
22

33
MCP remains an open [native release gate](NATIVE_MIGRATION.md). The Rust stdio
4-
client foundation is implemented and tested. Application registration,
5-
workspace/content activation, agent approvals, Settings/CLI integration,
6-
Streamable HTTP, built-in SQLite compatibility, and the ShadowCode MCP server
7-
still need their native implementation and verification. The stdio module alone
8-
does not enable MCP tools in the application.
4+
client now connects to native Settings, CLI registration, project activation,
5+
and individually approved agent tool calls. Streamable HTTP, built-in SQLite
6+
compatibility, the ShadowCode MCP server, and broader interoperability/real-model
7+
verification are still being migrated. Existing URL definitions remain visible
8+
and inactive; this development build does not silently launch the Python client.
9+
10+
## Enable external tools
11+
12+
In **Settings → MCP**, register a server with its name and a JSON array containing
13+
the executable and literal arguments, then review and enable it for the current
14+
trusted project. Registration and catalog refresh do not execute the command.
15+
Four servers may be enabled per project. A grant records the project's canonical
16+
path, the definition ID, and its SHA-256 content hash.
17+
18+
Project definitions are direct `.yaml`, `.yml`, or `.json` files in
19+
`.shadowcode/mcp/` or `.shadow/mcp/`. For example:
20+
21+
```json
22+
{
23+
"name": "my-tools",
24+
"command": ["/absolute/path/to/mcp-server", "--stdio"],
25+
"description": "Tools for this project",
26+
"timeout_sec": 30,
27+
"env_refs": { "API_TOKEN": "MY_TOOLS_API_KEY" }
28+
}
29+
```
30+
31+
`env_refs` maps child environment variables to names in the protected application
32+
secret store or the application's environment. Referenced values resolve only
33+
when a connection starts. Legacy literal `env` entries remain supported, but
34+
catalogs show only their variable names. Configured environment values are
35+
redacted from returned tool metadata and results; stderr is not put in task
36+
history. Avoid placing credentials in executable arguments, URLs, descriptions,
37+
or task prompts. External servers run with the user's OS privileges; activation
38+
is not an OS sandbox or a guarantee of network confinement.
39+
40+
The same controls are available without a display:
41+
42+
```sh
43+
shadowcode mcp add /path/to/definition.json
44+
shadowcode --json mcp
45+
shadowcode mcp enable config:my-tools --hash HASH_FROM_CATALOG
46+
shadowcode mcp disable config:my-tools
47+
shadowcode mcp remove config:my-tools --hash HASH_FROM_CATALOG
48+
```
49+
50+
Project IDs use `project:.shadowcode/mcp/my-tools.yaml`. Global IDs use
51+
`config:my-tools`. Replacing a global definition with `mcp add` requires its
52+
current `--hash`. Removing it also removes its project grants, so re-registering
53+
identical content does not reactivate it. Project overlays cannot grant access.
54+
Changed or missing project definitions must be reviewed or disabled before a
55+
new Build task uses them.
56+
57+
Build tasks with enabled servers receive two compact tools: `mcp_tools` discovers
58+
servers, lists tool names in pages of 20, or reads one exact tool schema;
59+
`mcp_call` requests a call to a server/tool with an argument object. Initialization
60+
is lazy and uses the explicit launch grant. Every `mcp_call` requires a separate
61+
session-scoped approval, even if the peer marks its tool read-only. The server
62+
ID, tool name, and complete arguments appear in that approval. Plan, Review,
63+
and untrusted projects cannot start MCP processes.
64+
65+
Running and queued tasks retain their configuration snapshot. Disabling a grant
66+
or replacing a global registration applies to new tasks; cancel existing tasks
67+
to stop their current connections. Project files are re-read and their hash
68+
checked before each external action, including after an approval wait.
69+
Definitions cannot replace their command while an earlier approval is pending.
70+
MCP actions invalidate earlier file observations and disable parallel native
71+
reads for that task. Changes made by an external server are not checkpointed by
72+
ShadowCode's file tools.
973

1074
## Stdio client foundation
1175

@@ -19,7 +83,7 @@ The omitted crate license is retained from that same commit in
1983
The low-level `Client::connect` API starts only the literal command supplied by
2084
its caller, in a canonical project directory. It does not discover or activate
2185
servers. Callers must obtain server activation and exact tool approval before
22-
using it; those application controls are the next integration step. A server's
86+
using it; the application runner supplies those controls. A server's
2387
tool annotations, descriptions or instructions cannot supply approval.
2488

2589
The child receives basic path/home/locale/XDG environment variables and explicitly
@@ -51,6 +115,8 @@ Unsolicited sampling fails and elicitation is declined. Interactive or backgroun
51115
task continuations returned from a tool are rejected rather than automatically
52116
supplying inputs or starting an unattended polling loop. A failed call does not
53117
prove that the remote tool made no changes; the client never retries it silently.
118+
A closed connection cannot return its old catalog as if it were still live;
119+
start a new task to reconnect after a protocol failure.
54120

55121
## Ownership and cancellation
56122

@@ -63,13 +129,17 @@ Processes that deliberately escape their group are outside this user-level
63129
cleanup boundary.
64130

65131
`Client::close` waits for cleanup. Dropping a client or abandoning its connection
66-
future triggers cleanup asynchronously; application task integration must await
67-
close before reporting task completion or releasing its workspace reservation.
132+
future immediately signals its group to stop; the cleanup worker reaps it.
133+
The application task loop awaits all connections before reporting completion or
134+
releasing the workspace, including provider errors, step limits, cancellation,
135+
and shutdown. Connections are scoped to one task and reused within that task.
136+
Durable `mcp.connected` and `mcp.closed` events retain the definition ID/hash
137+
and cleanup outcome without environment values.
68138

69139
## Verification
70140

71141
```sh
72-
cargo test -p shadowcode-core --test mcp_stdio --locked
142+
cargo test -p shadowcode-core --test mcp_stdio --test mcp_application --locked
73143
```
74144

75145
Ten regressions exercise real subprocess pipes, current and legacy protocol
@@ -80,6 +150,17 @@ stubborn descendant cleanup, leader reaping, stderr floods, protocol floods, and
80150
refusal of unsolicited sampling/credential requests. The small Node peer is a
81151
test fixture; no Node or Python runtime is added to the application.
82152

83-
These checks validate the client foundation. They do not stand in for the
84-
remaining application permission, UI, HTTP, server, interoperability and real
85-
local-model checks required for the full MCP migration.
153+
Eight application regressions cover inert/symlink-confined discovery, malformed
154+
definitions, hidden credentials, project trust and permissions, definition
155+
hashes, removal/re-registration, exact argument approval and denial, connection
156+
reuse, error-result preservation, bounded single-pass credential redaction,
157+
closed-catalog rejection, stale file observations, read-only tasks, and
158+
process cleanup before actual engine completion on every exit path. The native
159+
CLI and real-window probes also exercise their respective controls; the window
160+
probe drives a scripted model through discovery, approval, a real subprocess
161+
tool result, and cleanup.
162+
163+
These checks do not stand in for the remaining HTTP, server, interoperability,
164+
and real local-model checks required for the full MCP migration. The standalone
165+
application does not bundle runtimes for third-party servers: install whatever
166+
an explicitly selected external command requires separately.

‎docs/NATIVE_MIGRATION.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,11 @@ The remaining orchestration/integrations, full desktop interaction/stress
146146
coverage, and release packaging are still in progress. The native
147147
branch is not yet a replacement for the 0.19 release.
148148

149-
The [native MCP transport foundation](NATIVE_MCP.md) now has a bounded Rust stdio
150-
client and subprocess regressions. MCP application activation, agent-tool
151-
integration, HTTP transport and server migration remain open requirements.
149+
The [native MCP client](NATIVE_MCP.md) has bounded Rust stdio transport,
150+
inert Settings/CLI registration, explicit project/content activation, lazy tool
151+
discovery, individually approved calls, and task-owned process cleanup.
152+
HTTP transport, built-in SQLite compatibility, server migration, and broader
153+
interoperability/real-model verification remain open requirements.
152154

153155
AppImage extraction now uses a source-pinned runtime patch with private
154156
per-invocation directories, signal forwarding, and checked cleanup. A dedicated

‎docs/NATIVE_VERIFICATION.md‎

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ remain in [NATIVE_MIGRATION.md](NATIVE_MIGRATION.md).
66

77
## Automated checks
88

9-
`cargo fmt --all --check`, Clippy with warnings denied, and all **117 native
9+
`cargo fmt --all --check`, Clippy with warnings denied, and all **136 native
1010
integration tests** pass on the development machine. The suite covers:
1111

1212
- Config validation, private secrets, untrusted project overlays, profile locks,
@@ -71,6 +71,13 @@ integration tests** pass on the development machine. The suite covers:
7171
checks, queued manifest changes, compaction and
7272
provider failure. Output floods, timeouts, cancellation and engine shutdown
7373
assert that recorded parent/child processes actually stop.
74+
- MCP stdio negotiation, framing/catalog bounds, exact arguments, error results,
75+
denied sampling/elicitation, inert registration, workspace/hash activation,
76+
literal environment privacy, single-pass credential redaction, exact tool-call
77+
approval and denial, stale definitions during approval, read-only exclusion,
78+
connection reuse and closed-catalog rejection. Engine success, provider errors,
79+
step limits, cancellation and shutdown all wait for process cleanup. The test
80+
peer is Node-based test infrastructure, not a bundled application dependency.
7481

7582
The host's distro `rustdoc` needs its LLVM library directory in the loader path
7683
for doc tests. The full suite was run with:
@@ -94,6 +101,12 @@ and disables it. There are nine scenario groups with 27 scripted model requests.
94101
Actual pseudo-terminal sessions cover approval, denial, and Ctrl-C while a
95102
prompt is waiting. Noninteractive requests never silently grant approval.
96103

104+
The MCP update passes 11 CLI scenario groups with 30 scripted model requests.
105+
It adds inert server registration, hidden environment values, exact-hash
106+
activation, disable/removal, refusal without a terminal, and actual PTY approval
107+
of a displayed server/tool/argument object. A real MCP subprocess returns the
108+
expected result and its recorded process group is stopped before CLI exit.
109+
97110
The probe also hosts `serve`, starts and stops background commands, reads their
98111
logs, runs a task alongside them, observes and cancels detached work, and approves
99112
a waiting task from another CLI. SIGINT/SIGTERM/SIGHUP, a closed stdout pipe, remote
@@ -122,7 +135,7 @@ sessions existed. A regression now covers real session listing and literal
122135
search. Visual inspection also found a cancellation transcript race and an open
123136
sidebar obscuring a resized compact window; the window test checks both.
124137
Axe WCAG 2 A/AA and 2.1 AA checks pass in light, dark, compact, Goals, Router,
125-
Background, Skills, and Hooks workspace views. The compact check caught the Review button losing its accessible name
138+
Background, Skills, Hooks, and MCP workspace views. The compact check caught the Review button losing its accessible name
126139
when its text was hidden; the control now retains an explicit label.
127140
The native test also creates a goal through the drawer, completes all three
128141
milestones, approves its verification command, checks the resulting file and
@@ -239,10 +252,27 @@ deterministic fork regression reproduces that failure in the old implementation;
239252
the shared guard still keeps the profile locked while owned background cleanup
240253
is running. A forked copy cannot unlock its parent's live guard.
241254

255+
The profile-lock fix and runtime source changes passed the complete
256+
[CI run for 0f6de80](https://github.com/ShadowfetchLinux/ShadowCode/actions/runs/35522325919),
257+
including clean-runner compilation, both package formats, offline runtime source
258+
rebuild, runtime lifecycle stress, and the packaged CLI/window. The later MCP
259+
integration is recorded separately; this green run does not certify it.
260+
242261
OS dialog interaction, notification delivery, broader stress/accessibility
243262
coverage, corresponding sources for remaining redistributed components,
244263
and the final installed release still need their release-gate checks.
245264

265+
The MCP window update passes 17 scripted model requests and nine Axe views. It
266+
registers a server through Settings, confirms registration is inert, enables the
267+
reviewed definition, displays the exact call arguments, approves an actual MCP
268+
subprocess request, checks the redacted result reaches the model, waits for
269+
process cleanup, and disables/removes the registration. Visual review uses
270+
`artifacts/native/mcp.png`; the complete run is recorded in
271+
`artifacts/native/result.json`. The test exposed notifications covering Close
272+
and Send controls; dialogs now stay above transient messages and notification
273+
bodies no longer intercept clicks. The registration form collapses after adding
274+
a server so its reviewed command and controls stay in view.
275+
246276
## Real local models
247277

248278
`scripts/probe-native-hooks.mjs` tests an installed Ollama model through the

‎native/core/src/cli/args.rs‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ pub enum Command {
8585
#[arg(long, requires = "enable")]
8686
hash: Option<String>,
8787
},
88+
/// List inert MCP definitions, register one, or review a project activation.
89+
Mcp {
90+
#[command(subcommand)]
91+
action: Option<Mcp>,
92+
},
8893
/// List/search saved conversations, or rename/delete a unique ID prefix.
8994
Sessions {
9095
query: Option<String>,
@@ -157,6 +162,30 @@ pub enum Command {
157162
/// Run the shared engine without a window until Ctrl-C or SIGTERM.
158163
Serve,
159164
}
165+
#[derive(Debug, Subcommand)]
166+
pub enum Mcp {
167+
/// Register a JSON/YAML definition; does not launch or enable it.
168+
Add {
169+
definition: PathBuf,
170+
/// Current definition hash when replacing an existing registration.
171+
#[arg(long)]
172+
hash: Option<String>,
173+
},
174+
/// Allow this exact server definition to start for new Build tasks here.
175+
Enable {
176+
server: String,
177+
#[arg(long)]
178+
hash: String,
179+
},
180+
/// Revoke this project's activation for future tasks.
181+
Disable { server: String },
182+
/// Remove a global registration and its project activations.
183+
Remove {
184+
server: String,
185+
#[arg(long)]
186+
hash: String,
187+
},
188+
}
160189
#[derive(Debug, Args)]
161190
pub struct Run {
162191
pub task: String,

‎native/core/src/cli/mod.rs‎

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use crate::{
2121
};
2222
use anyhow::{bail, ensure, Context, Result};
2323
pub use args::Options;
24-
use args::{Background, Command, Run, TaskOptions};
24+
use args::{Background, Command, Mcp, Run, TaskOptions};
2525
use backend::Backend;
2626
use clap::Parser;
2727
use serde_json::{json, Value};
@@ -358,6 +358,56 @@ async fn execute(backend: &Backend, workspace: &Path, options: &Options) -> Resu
358358
backend.call("GET", "/api/hooks", Value::Null).await?
359359
}
360360
}
361+
Command::Mcp { action } => match action {
362+
None => backend.call("GET", "/api/mcp/servers", Value::Null).await?,
363+
Some(Mcp::Add { definition, hash }) => {
364+
use std::io::Read;
365+
let file = std::fs::File::open(definition).context("Cannot open MCP definition")?;
366+
ensure!(
367+
file.metadata()?.is_file(),
368+
"MCP definition must be a regular file"
369+
);
370+
let mut text = String::new();
371+
file.take(32_001).read_to_string(&mut text)?;
372+
ensure!(text.len() <= 32_000, "MCP definition exceeds 32 KB");
373+
let definition: Value = serde_yaml_ng::from_str(&text)
374+
.map_err(|_| anyhow::anyhow!("Invalid MCP JSON/YAML definition"))?;
375+
backend
376+
.call(
377+
"POST",
378+
"/api/mcp/servers",
379+
json!({"definition":definition,"hash":hash.as_deref().unwrap_or("")}),
380+
)
381+
.await?
382+
}
383+
Some(Mcp::Enable { server, hash }) => {
384+
backend
385+
.call(
386+
"POST",
387+
"/api/mcp/activation",
388+
json!({"workspace":workspace,"server":server,"hash":hash,"enabled":true}),
389+
)
390+
.await?
391+
}
392+
Some(Mcp::Disable { server }) => {
393+
backend
394+
.call(
395+
"POST",
396+
"/api/mcp/activation",
397+
json!({"workspace":workspace,"server":server,"enabled":false}),
398+
)
399+
.await?
400+
}
401+
Some(Mcp::Remove { server, hash }) => {
402+
backend
403+
.call(
404+
"POST",
405+
"/api/mcp/servers/delete",
406+
json!({"server":server,"hash":hash}),
407+
)
408+
.await?
409+
}
410+
},
361411
Command::Status => {
362412
let mut status = backend
363413
.call("GET", "/api/workspace/status", Value::Null)

0 commit comments

Comments
 (0)