Skip to main content
Glama
README.md
# 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: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)]()
[![MCP](https://img.shields.io/badge/MCP-stdio-purple.svg)]()

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

A3.8/5.0

Scored across 5 tools

Disambiguation4/5

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.

Naming Consistency5/5

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.

Tool Count5/5

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.

Completeness3/5

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.

Maintenance

ActivitySlowing
ResponsivenessNo issues