dsh-web-mcp
# dsh-web-mcp
> ๐ค **AI-assisted project** ยท A college student's learning project, with significant code generated by an LLM agent under human direction and review.
[](LICENSE)
[]()
[]()
MCP bridge for [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) web UI โ exposes the cordis RPC API over HTTP `/api/<endpoint>` as stdio MCP tools. Any MCP client (Codex CLI, Claude Desktop, etc.) can drive DSH sessions and benefit from prompt-prefix cache reuse.
**๐ ไธญๆๆๆกฃ๏ผ[README.zh.md](README.zh.md)**
## What is in the box
Eight tools, all backed by DSH web's cordis RPC over HTTP `/api/<endpoint>`:
| Tool | Purpose |
|---|---|
| `dsh_list_workspaces` | Enumerate every workspace known to the running DSH web UI. |
| `dsh_create_session` | Adopt a directory (creating the workspace if missing), then create a session bound to it. Selects the model in one shot. Returns `sessionId`. |
| `dsh_send_message` | Send a prompt and **block** until the assistant turn completes. Returns the assistant text plus per-turn token usage including `cacheReadTokens` / `cacheWriteTokens`. When the agent requests permission, it answers via an MCP **sampling callback** (see below) so the turn keeps running. |
| `dsh_wait_turn` | Wait for the in-flight turn to finish **without** sending a new prompt. Use it after answering a pending approval. |
| `dsh_list_pending_approvals` | List still-pending approval requests (`approvalId`, `toolName`, `callId?`, `reason?`, `rpcId`), optionally filtered by `session_id`. |
| `dsh_respond_approval` | Answer one pending approval (`allowed-once` / `rejected`); returns the DSH receipt (`accepted: true` = the answer was consumed). |
| `dsh_get_session_stats` | Fetch cached projections: `tokenUsage`, `sessionStats`, `contextPressure`. |
| `dsh_resume_session` | Verify a session is still alive and surface its current model. Subsequent `dsh_send_message` calls reuse the prompt prefix. |
## Approval callback (ๆ้ๅฎกๆนๅ่ฐ)
DSH agents ask for permission before sensitive tool calls (e.g. a sandbox
escalation to `danger-full-access`). The request surfaces in the session log
as `approval/asked` and โ on the DSH web event stream โ as an answerable
`approval/requested` frame. This bridge turns that into a two-track callback:
1. **Sampling callback (default, works with Hermes CN Desktop โฅ 0.18)**: while
`dsh_send_message` / `dsh_wait_turn` are waiting for the turn, the server
sends the client a `sampling/createMessage` request describing the tool and
the DSH-provided reason; the client's answer (`allowed-once` / `rejected`)
is posted back to DSH via `POST /api/respond` and the turn continues.
Hermes supports this out of the box (`sampling` enabled by default; see its
*Native MCP* docs). Sampling needs to run inside an MCP request, so it is
only active on the real MCP transport.
2. **Tool fallback (works with any MCP client)**: set
`auto_respond_approvals=false` on `dsh_send_message` (or sampling fails /
is unavailable), and the call returns immediately with
`awaitingApproval: true` + `pendingApprovals` instead of waiting. The
caller (or a human) then decides:
```text
dsh_send_message(...) -> {"awaitingApproval": true, "pendingApprovals": [...]}
dsh_respond_approval(session_id, <approvalId>, "allowed-once") -> {"accepted": true}
dsh_wait_turn(session_id, ...) -> normal turn result
```
`dsh_list_pending_approvals` lists whatever is still pending at any moment
(the DSH event stream replays every unanswered approval on connect).
Wire facts (verified against DSH source and a live probe): the answer must
echo the `rpcId` of the `approval/requested` frame โ a fresh UUID minted by
the host's pending table, **not** the audit `approvalId` โ and the payload
must carry the matching `approvalId`. The event stream is a **WebSocket**
(`GET /api/events.mux`; plain HTTP gets 426), which is why the `websockets`
package is a hard dependency.
## Requirements
- Python 3.10+
- A running `dsh web` instance on `http://127.0.0.1:3080` (override with `DSH_BASE_URL`)
- `uv` (recommended) or `pip`
- `websockets` (installed by `uv sync`; needed for the approval event stream)
## Install
```powershell
git clone https://github.com/cv588888888888888888888ju/dsh-web-mcp.git
cd dsh-web-mcp
uv sync
```
Then run as a stdio MCP server:
```powershell
uv run dsh-web-mcp
```
## Wire up Hermes Agent
Register with `hermes mcp add`:
```powershell
hermes mcp add dsh --command uv --args --directory C:\Users\chenty\Documents\feishu-bot\dsh-mcp run dsh-web-mcp
```
> โ ๏ธ **Known pitfall (tested)**:
> - Do **not** pass `--env DSH_BASE_URL=...` โ it gets forwarded to `dsh-web-mcp`'s argparse and fails with `unrecognized arguments`. Set `DSH_BASE_URL` as a user/system environment variable instead.
> - A **new session** is required after registering (config loads at session start).
> - If prompted `Enable all 8 tools? [Y/n/select]`, answer `Y`.
## Wire up Codex CLI
Edit `%USERPROFILE%\.codex\config.toml` (or `~/.codex/config.toml` on other OS):
```toml
[mcp_servers.dsh]
command = "uv"
args = ["--directory", "C:\\path\\to\\dsh-web-mcp", "run", "dsh-web-mcp"]
# Optional, defaults to http://127.0.0.1:3080 if omitted
[mcp_servers.dsh.env]
DSH_BASE_URL = "http://127.0.0.1:3080"
```
Restart Codex CLI. The eight `dsh_*` tools appear alongside the built-in tools.
## Probe
`probe.py` is a developer-side smoke test that exercises all tools end-to-end against the running DSH web UI โ including the approval chain (trigger a real approval, answer it with `accepted: true`, and wait for the turn to finish):
```powershell
uv run python probe.py
```
It prints a JSON blob per step; expected outcome is
```json
{
"ok": true,
"step": "send_message",
"reply_contains": "TASK_OK",
"cacheReadTokens": 8192
}
```
The approval steps report `send_message_awaiting_approval`, `respond_approval`
(`accepted: true`), and `wait_turn_after_approval` (`reply_contains: "APPROVAL_OK"`).
## Failure modes
- **DSH web not reachable** โ the server starts but every tool returns `{"ok": false, "error": "DSH web not reachable at ..."}`. Make sure `dsh web` is running (`dsh web --port 3080`).
- **DSH schema drift (rc.X โ rc.Y)** โ unknown field errors come back as `{ok: false, error: "dsh returned <code>: <msg>"}`. The model schema in `models.py` is intentionally `extra="allow"` so additional fields pass through; reported mis-parses should be filed against `models.py`.
- **Prompt timeout** โ `dsh_send_message` times out after `timeout_s` (default 120s); rerun with a larger value if your prompt is long. An approval that nobody answers also holds the turn: use `dsh_list_pending_approvals` / `dsh_respond_approval` (or wait for the human in the DSH web UI) and then `dsh_wait_turn`.
- **Sampling unavailable** โ outside an MCP request (e.g. `probe.py`) or with a client that does not implement `sampling/createMessage`, `dsh_send_message` falls back to returning `awaitingApproval: true` with `pendingApprovals`; use the tool fallback above.
## Configuration
Environment variables read by `dsh-web-mcp`:
| Var | Default | Description |
|---|---|---|
| `DSH_BASE_URL` | `http://127.0.0.1:3080` | DSH web base URL. |
| `DSH_TIMEOUT_S` | `60` | Per-request timeout in seconds (generous; a single LLM turn may take ~30s). |
| `DSH_MCP_LOG` | `INFO` | Python `logging` level (use `DEBUG` to see wire-level traffic). |
CLI flags mirror the env vars: `--base-url`, `--timeout`, `--check`.
## Why this exists
By default Codex CLI talks to the OpenAI / Azure providers directly. When MCP routing through DSH, the `deepseek-v4-flash` preset in DSH keeps system + tools + conversation prefix cached, so every subsequent turn in the same session reads 8K+ cached tokens and only pays uncached input for the new prompt + uncached output for the new reply โ measured per `tokenUsage.cacheReadTokens` in `dsh_send_message` results.
## Background
This project was built as a learning exercise by an undergraduate student exploring agent tooling. The bulk of the code was generated by an LLM coding agent (Codex CLI + DeepSeek) under human direction; every line was reviewed and the behavior verified end-to-end before publication. Bugs are likely given the author's experience level โ please open issues.
## Status
Pre-release. API surface follows DSH 0.1.0-rc.6 schema (`rpc-map.d.ts`); regenerate from source if you bump DSH.
## License
[MIT](LICENSE)
TDQS
Scored across 5 tools
Tool purposes are mostly distinct, but create_session and resume_session both establish a session for sending messages, and the difference (new vs existing) could confuse an agent without careful reading. Descriptions resolve the ambiguity, so only minor overlap remains.
All tools share the dsh_ prefix and follow a clean verb_noun pattern: create_session, send_message, get_session_stats, resume_session, list_workspaces. The naming is perfectly consistent and predictable.
With 5 tools, the server is well-scoped for session and workspace management. Each tool has a clear role, and the count is within the ideal range for a purpose-built MCP.
Core workflow is covered (create/resume, send, stats, list workspaces), but the surface lacks session deletion or a way to list existing sessions directly. These are notable lifecycle gaps that agents may hit when trying to manage or clean up sessions.