Skip to content

Commit 8f95d90

Browse files
Serve native MCP over authenticated loopback HTTP
1 parent 413a01e commit 8f95d90

17 files changed

Lines changed: 1233 additions & 46 deletions

‎.github/workflows/native.yml‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,13 @@ jobs:
3232
run: node scripts/test-native-cli.mjs
3333
- name: Exercise the native MCP server without a display
3434
run: node scripts/test-native-mcp-server.mjs
35+
- name: Exercise the authenticated native MCP HTTP server
36+
run: SHADOW_MCP_TRANSPORT=http node scripts/test-native-mcp-server.mjs
3537
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
3638
if: always()
3739
with:
3840
name: native-mcp-server-diagnostics
39-
path: artifacts/native-mcp-server
41+
path: artifacts/native-mcp-server*
4042
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
4143
if: always()
4244
with:
@@ -80,6 +82,13 @@ jobs:
8082
SHADOW_CLI_ARGS: '["--appimage-extract-and-run"]'
8183
SHADOW_MCP_ARTIFACTS: artifacts/native-package/mcp-server
8284
run: node scripts/test-native-mcp-server.mjs
85+
- name: Exercise the packaged native MCP HTTP server
86+
env:
87+
SHADOW_DESKTOP_BINARY: ${{ github.workspace }}/target/release/bundle/appimage/ShadowCode_0.20.0_amd64.AppImage
88+
SHADOW_CLI_ARGS: '["--appimage-extract-and-run"]'
89+
SHADOW_MCP_ARTIFACTS: artifacts/native-package/mcp-server-http
90+
SHADOW_MCP_TRANSPORT: http
91+
run: node scripts/test-native-mcp-server.mjs
8392
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
8493
if: always()
8594
with:

‎CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
## 0.20.0 — native development (unreleased)
44

5+
- Native MCP serving supports authenticated loopback Streamable HTTP alongside
6+
stdio. Both expose the same project-scoped tools, approvals and owned tasks.
7+
HTTP reconnects preserve the gateway's jobs; gateway shutdown cancels unfinished
8+
owned work while unrelated engine tasks continue. Mandatory credentials, host
9+
checks, denied browser origins, bounded traffic and protocol validation apply.
10+
The executable probe runs both transports against source and packaged builds.
11+
512
- Rust task engine with confined file tools, bounded processes, streaming local
613
and compatible models, scoped approvals, checkpoints, persisted queues, and
714
recovery. Settings and legacy history are migrated with backups.

‎Cargo.lock‎

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ workflows, [managed background processes and model tools](docs/NATIVE_BACKGROUND
2222
[native SQLite inspection](docs/NATIVE_SQLITE.md),
2323
[queued follow-ups](docs/NATIVE_QUEUE.md),
2424
[approved MCP stdio and HTTP tools](docs/NATIVE_MCP.md), a
25-
[native MCP stdio server](docs/NATIVE_MCP.md#connect-another-coding-tool-to-shadowcode), and a
25+
[native MCP stdio and authenticated HTTP server](docs/NATIVE_MCP.md#connect-another-coding-tool-to-shadowcode), and a
2626
[native CLI](docs/NATIVE_CLI.md) that shares the active desktop engine or runs
2727
headlessly with its own profile.
2828
Development AppImage and Debian

‎docs/NATIVE_CLI.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,11 +150,14 @@ Disabling affects new tasks; cancel running and queued tasks to end their
150150
existing grants. HTTP definitions use `url` and an optional `api_key_env` bearer
151151
secret reference; remote hosts require network access in Permissions.
152152

153-
To expose ShadowCode itself to another MCP client, use `mcp serve` over stdio.
153+
To expose ShadowCode itself to another MCP client, use `mcp serve` over stdio, or
154+
`mcp serve --http 127.0.0.1:8765 --token-env SHADOW_MCP_HTTP_TOKEN` for authenticated
155+
loopback HTTP. Set that environment/profile secret before starting the gateway.
154156
`mcp register` prints generic JSON for that executable, project and explicit
155157
profile. Both default to read-only access. `--allow-write` delegates changes
156158
within the trusted project's configured permissions; adding `--allow-approvals`
157-
also delegates individual approvals for that connection's own tasks. Run
159+
also delegates individual approvals for that MCP owner's tasks. HTTP reconnects
160+
share the same gateway owner; separate gateways keep clients independent. Run
158161
`mcp serve` without `--json` so stdout remains protocol-only. See the
159162
[native server's tools, lifecycle and remaining limits](NATIVE_MCP.md#connect-another-coding-tool-to-shadowcode).
160163

‎docs/NATIVE_DESKTOP.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@ each exact call. HTTP bearer secrets resolve only on connection, and remote
8787
hosts require network access. Connections close before the task finishes.
8888
`shadowcode mcp serve` also exposes the selected project to another client over
8989
stdio, with read-only defaults and explicitly delegated write/approval access.
90-
The [server guide](NATIVE_MCP.md#connect-another-coding-tool-to-shadowcode) lists
90+
The native executable also serves authenticated loopback HTTP with a fixed
91+
project and a gateway owner that survives individual client reconnects. The
92+
[server guide](NATIVE_MCP.md#connect-another-coding-tool-to-shadowcode) lists
9193
the implemented tools and remaining migration requirements.
9294

9395
To work on the live interface, run `./ui/node_modules/.bin/tauri dev` from the

‎docs/NATIVE_MCP.md‎

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33
MCP remains an open [native release gate](NATIVE_MIGRATION.md). The Rust stdio and Streamable HTTP
44
clients now connect to native Settings, CLI registration, project activation,
5-
and individually approved agent tool calls. The native stdio server now exposes
6-
tasks, goals, reviews, memory, SQLite inspection and checkpoints to external clients.
7-
The remaining server features and broader interoperability/real-model
8-
verification are still being migrated. Neither direction launches Python.
5+
and individually approved agent tool calls. Native stdio and authenticated loopback
6+
HTTP servers expose tasks, goals, reviews, memory, SQLite inspection and checkpoints
7+
to external clients. Client-specific registration and broader interoperability/real-model
8+
verification remain release requirements. Neither direction launches Python.
99

1010
## Connect another coding tool to ShadowCode
1111

12-
The native executable can serve a single MCP client without a display:
12+
The native executable can serve a single MCP client over stdio without a display:
1313

1414
```sh
1515
shadowcode --workspace /absolute/project mcp serve
@@ -41,7 +41,7 @@ Pending actions can be approved through ShadowCode's desktop or CLI. To delegate
4141
approval decisions to the connecting client, explicitly add **both**
4242
`--allow-write --allow-approvals` to `mcp serve` (or to `mcp register` when producing
4343
its configuration). The `shadow_approve` tool then resolves one exact pending
44-
approval from a task created by this connection. It cannot approve another
44+
approval from a task created by this MCP owner (the stdio connection or HTTP gateway). It cannot approve another
4545
client's task or all pending actions. Denying an owned approval does not require
4646
the approval flag. The client is responsible for obtaining the user's agreement
4747
to the displayed action before submitting approval.
@@ -63,7 +63,7 @@ The current server exposes seventeen tools:
6363
- `shadow_goal` creates, lists, inspects, advances or abandons durable goals.
6464
Creating or advancing a goal here does not start an agent automatically.
6565
- `shadow_run` creates a fresh conversation and returns its job ID. `shadow_jobs`
66-
inspects that connection's jobs, paginated events and pending approvals, or
66+
inspects that owner's jobs, paginated events and pending approvals, or
6767
cancels one. A returned job ID is not a claim that the task has completed.
6868
- `shadow_approve` handles the individual decision described above.
6969
- `shadow_test` submits an owned native test job without calling a model. An
@@ -78,7 +78,7 @@ The plan resource reads recorded events and returns null when no plan is recorde
7878
The `delegate` prompt accepts a task for the fixed project; `understand` requests
7979
a read-only project inspection.
8080

81-
The connection owns its delegated jobs. Normal EOF, protocol failure, explicit
81+
A stdio connection owns its delegated jobs. Normal EOF, protocol failure, explicit
8282
cancellation and an abandoned server future cancel unfinished owned jobs and await
8383
cleanup; unrelated tasks in the shared engine continue. A temporary engine closes
8484
when its server exits. The first delegated task opens a private ownership socket
@@ -101,16 +101,68 @@ close ownership; valid rejected task submissions leave it usable. Submissions
101101
and cleanup acknowledgements have 30-second response deadlines. Normal close
102102
waits for the engine's task cleanup before acknowledging it.
103103

104-
Incoming messages share the 1-MiB frame, 32-MiB lifetime and 128-frames-per-second
104+
Incoming stdio messages share the 1-MiB frame, 32-MiB lifetime and 128-frames-per-second
105105
transport limits. Initialization has a ten-second deadline. Up to eight tool or
106106
resource operations and 64 delegated tasks are allowed per connection. Other
107107
operations have a 45-second deadline; model jobs are asynchronous and follow the
108108
engine's own limits. Tool/resource results are capped at 2 MB and response writes
109109
have a five-second deadline. Reconnect after a connection limit is reached.
110110

111+
### Authenticated HTTP gateway
112+
113+
Run a persistent gateway for the selected project with an independently generated
114+
credential. `--token-env` names an environment variable or private profile secret;
115+
the actual token is not passed as a command-line argument:
116+
117+
```sh
118+
export SHADOW_MCP_HTTP_TOKEN="$(openssl rand -hex 32)"
119+
shadowcode --workspace /absolute/project mcp serve \
120+
--http 127.0.0.1:8765 --token-env SHADOW_MCP_HTTP_TOKEN
121+
```
122+
123+
Configure the calling application's Streamable HTTP transport with
124+
`http://127.0.0.1:8765/mcp` and `Authorization: Bearer <the secret>`. The gateway
125+
announces its ready address on stderr; stdout stays empty. Port `0` selects an
126+
available port, which is reported after the engine is ready. Stdin EOF does not
127+
stop HTTP serving. SIGINT/SIGTERM stop the gateway and wait for owned cleanup.
128+
`mcp register` still emits only generic stdio configuration.
129+
130+
The same read-only default, trust requirements and optional
131+
`--allow-write --allow-approvals` flags apply. A gateway is one fixed-project owner
132+
shared by every caller holding its credential. Reconnecting over HTTP preserves
133+
its delegated jobs and their approval scope. Use separate gateway invocations
134+
and credentials for independent clients. Each gateway has its own engine ownership
135+
lease; stopping or killing it cancels its unfinished running and queued jobs,
136+
without stopping another gateway's or the desktop's tasks. Restart after the
137+
64-job lifetime limit. Project background processes retain their independent
138+
lifetime described above.
139+
140+
Only loopback IP binds are accepted. Credentials must contain 32–512 bearer-token
141+
characters; generate them randomly. Authentication is mandatory even on loopback.
142+
Only the bound IP or `localhost` with the actual port is accepted as Host. Browser
143+
Origin headers, query parameters and duplicate MCP metadata headers are rejected;
144+
credential values are removed before requests reach MCP handlers. This endpoint
145+
does not provide remote deployment, browser CORS, TLS or OAuth.
146+
147+
The official Rust SDK supplies current per-request protocol negotiation and legacy
148+
Streamable HTTP initialization. The gateway is stateless at the protocol layer:
149+
it issues no MCP session ID and uses JSON responses, with request-scoped SSE when
150+
required by the SDK. GET and DELETE return 405. It does not implement the older
151+
separate `/sse` and message endpoints. See the MCP
152+
[Streamable HTTP specification](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http)
153+
and [legacy transport specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports).
154+
155+
HTTP permits 32 concurrent TCP connections and 128 requests per second. Each
156+
connection handles one request, with five-second header/body deadlines, a 1-MiB
157+
request limit, a 60-second request/response deadline and an 8-MiB transport response
158+
limit. Existing eight-operation concurrency, 45-second tool deadlines and 2-MB
159+
result limits still apply. Model/test jobs return an ID and continue asynchronously;
160+
a closed HTTP request is not a request to cancel an already submitted job. Use
161+
`shadow_jobs` cancellation or stop its gateway.
162+
111163
This is a development server, not full compatibility with the earlier Python
112-
server. HTTP serving and client-specific registration formats
113-
remain unimplemented. Tools use the fixed project and asynchronous owned jobs;
164+
server. Client-specific registration formats remain unimplemented.
165+
Tools use the fixed project and asynchronous owned jobs;
114166
map saving is explicit, and diagnostics do not perform automatic repairs.
115167
The current tests use the official
116168
Rust SDK and a real executable protocol probe; broader client interoperability

‎docs/NATIVE_MIGRATION.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,11 @@ transports, inert Settings/CLI registration, explicit project/content activation
161161
discovery, individually approved calls, and task-owned process/stream cleanup.
162162
HTTP uses explicit credential references, prevents redirects and automatic call
163163
retries, and supports modern and legacy Streamable HTTP negotiation. The native
164-
stdio server exposes seventeen tools, four resources and two prompts through
164+
stdio and authenticated loopback HTTP servers expose seventeen tools, four resources and two prompts through
165165
the shared engine, with a fixed project, reduced per-task permissions, explicit
166-
approval delegation and connection-owned jobs. HTTP serving and broader interoperability/real-model
167-
verification remain open requirements. Private engine-side ownership now cancels
166+
approval delegation and owned jobs. HTTP uses a gateway owner across stateless
167+
requests, with mandatory bearer authentication and bounded traffic. Client-specific
168+
registration and broader interoperability/real-model verification remain open requirements. Private engine-side ownership now cancels
168169
MCP jobs after gateway SIGKILL, including unread submission replies, queued tasks
169170
and active command children; unrelated work continues.
170171

‎docs/NATIVE_VERIFICATION.md‎

Lines changed: 16 additions & 1 deletion
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 **188 native
9+
`cargo fmt --all --check`, Clippy with warnings denied, and all **193 native
1010
integration tests** pass on the development machine. The suite covers:
1111

1212
- Config validation, private secrets, untrusted project overlays, profile locks,
@@ -158,6 +158,21 @@ The seventeen-tool catalog includes project inspection, native diagnostics, SQLi
158158
approved test execution, which the probe exercises without extra model requests.
159159
These fixtures do not substitute for broader client and real-model interoperability.
160160

161+
The same executable probe now passes with `SHADOW_MCP_TRANSPORT=http`: six
162+
scripted model requests, modern per-request negotiation and legacy initialization,
163+
mandatory authentication, invalid bind/credential rejection, read-only defaults,
164+
real edits/tests, exact approvals, checkpoint rollback, and gateway SIGKILL cleanup.
165+
Each HTTP request opens a fresh connection; stdin EOF preserves the gateway and
166+
its delegated jobs. SIGTERM closes temporary owners cleanly. CI repeats both
167+
transports against the native binary and packaged AppImage.
168+
169+
Five Rust HTTP-server tests additionally exercise official-SDK interoperability,
170+
project and owner isolation, duplicate/mismatched metadata, invalid credentials,
171+
browser origins and hosts, malformed and oversized bodies, slow partial requests,
172+
reconnects, queued cancellation, child cleanup, and abandoned-future profile/socket
173+
release. Dependency-notice generation passes with 540 application dependencies,
174+
including the HTTP server's newly resolved `httpdate` dependency.
175+
161176
The host's distro `rustdoc` needs its LLVM library directory in the loader path
162177
for doc tests. The full suite was run with:
163178

‎native/core/Cargo.toml‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,11 @@ libc = "0.2"
2525
tracing = "0.1"
2626
cap-std = "3"
2727
clap = { version = "4.6.7", features = ["derive"] }
28-
rmcp = { version = "=3.4.0", default-features = false, features = ["client", "server", "transport-streamable-http-client"] }
28+
rmcp = { version = "=3.4.0", default-features = false, features = ["client", "server", "transport-streamable-http-client", "transport-streamable-http-server"] }
2929
sse-stream = "=0.2.6"
30+
bytes = "1"
31+
http-body = "1"
32+
http-body-util = "0.1"
33+
hyper = { version = "1", features = ["http1", "server"] }
34+
hyper-util = { version = "0.1", features = ["tokio"] }
35+
tower-service = "0.3"

0 commit comments

Comments
 (0)