Skip to content
dsh.fish
Bundle

dsh-mcp-live-status

Live MCP server status in the DeepSeek Harness conversation composer — see which MCP servers are actually connected before you hit send.

Source
felix-lj-ct
License
MIT
Updated
Updated 2 days ago

Readme

# dsh-mcp-live-status

English | [中文](README.zh.md)

[![Listed on dsh-plugin.org](https://dsh-plugin.org/badges/listed.svg)](https://dsh-plugin.org/plugins/felix-lj-ct/dsh-mcp-live-status)
[![npm](https://img.shields.io/npm/v/dsh-mcp-live-status)](https://www.npmjs.com/package/dsh-mcp-live-status)
[![license](https://img.shields.io/npm/l/dsh-mcp-live-status)](LICENSE)

**See which MCP servers are actually connected — in the composer, before you hit send.**

A DeepSeek Harness plugin that puts a live MCP status pill in the conversation
composer's tool row, next to the access-mode and model controls.

![The MCP status pill in the composer tool row](docs/screenshot.png)

## Features

- MCP connection status at a glance, inside the composer
- Catches the false-healthy case: mounted but never connected
- Click through for per-server transport, tool count, and failure reason
- Read-only and zero-config; command-line credentials are redacted

## Why

DSH's settings page can tell you an MCP plugin *mounted*. It cannot tell you the
server *answered*.

`@deepseek-ai/dsh-mcp-client` defaults to `failOnStartupError: false`, so a
server whose transport never connected still reaches Cordis fiber state
`ACTIVE`. It looks healthy everywhere in the UI. You find out it is dead when
the agent tries to call one of its tools, mid-task.

This plugin closes that gap by checking the one thing that actually proves a
handshake happened: **tool registration**. mcp-client only registers
`mcp__<serverName>__<toolName>` on `ctx.tools` after `connect()` *and*
`listTools()` have both succeeded. So the status is a join of two sources —
what the Loader says is configured, against what the tool registry says arrived.

| Loader entry | Registered tools | Shown as |
|---|---|---|
| fiber `active` | > 0 | 🟢 Connected |
| fiber `active` | 0 | 🟡 **Up, not connected** ← the one nothing else surfaces |
| fiber `pending` / `loading` | — | 🔵 Starting |
| fiber `failed` | — | 🔴 Mount failed |
| disabled | — | ⚪ Disabled (hidden by default) |

## Install

```bash
dsh plugin --profile web add dsh-mcp-live-status
```

Or straight from source, if you prefer not to go through npm:

```bash
dsh plugin --profile web add github:felix-lj-ct/dsh-mcp-live-status
```

Then restart the profile — a running instance keeps the old code in memory:

```bash
dsh --profile web
```

That is the whole setup. There is nothing to configure and no command to run:
open a session and the pill is in the composer tool row, just right of the
access-mode control.

**Two prerequisites**, both easy to miss:

1. **The `web` profile.** The plugin is browser UI; it does nothing in
   `headless` or `tui`.
2. **At least one configured MCP server.** With none, the pill renders nothing
   at all — by design, so an empty setup costs no layout. That is also the most
   common reason for "I installed it and see nothing".

### Not seeing the pill?

Check the host half first — it answers independently of the browser:

```bash
curl 127.0.0.1:3080/dsh-mcp-live-status/status
```

- **Connection refused / 404** — the host half is not loaded. Confirm the patch
  row landed: `dsh --profile web --dump-config | grep -A2 dsh-mcp-live-status`
- **`"configured": 0`** — no MCP servers are configured, or they are all
  disabled. Set `showDisabled: true` to see the disabled ones.
- **Valid JSON with servers, but no pill** — you are on a screen with no
  session. Open or start a conversation.

## What it looks like

```
Healthy — no denominator, because a denominator would carry no information
  [+] [⏱ Full access ⌄] [● MCP 4]        [model ⌄] [↑]

Degraded — the denominator appears exactly when something is wrong
  [+] [⏱ Full access ⌄] [● MCP 3/4]      [model ⌄] [↑]
```

Clicking the pill opens a read-only panel listing every server with its
transport, tool count, and — when something is off — the reason:

```
┌──────────────────────────────────┐
│ MCP servers                   ⟳  │
├──────────────────────────────────┤
│ ● atlassian     stdio    31 tools │
│ ● mongodb-qa    stdio    20 tools │
│ ● broken-probe  stdio  Up, not connected │
├──────────────────────────────────┤
│ 3 configured · 2 connected        │
└──────────────────────────────────┘
```

## Configuration

Optional. Defaults are fine for most people.

```yaml
- id: dsh-mcp-live-status
  name: dsh-mcp-live-status
  config:
    pollIntervalMs: 10000   # 0 disables polling (mount + manual refresh only)
    showDisabled: false     # include Loader entries that are disabled
```

Polling only runs while the browser tab is visible.

## Permissions and risk

Read-only. This plugin has no way to start, stop, reload, or reconfigure an MCP
server — managing servers stays with the settings page.

| Surface | What it does |
|---|---|
| `ctx.loader` | Reads the configured plugin tree (read-only iteration) |
| `ctx.tools` | Reads registered tool *names* only; never calls a tool |
| `ctx.webServer` | Serves one JSON route, `GET /dsh-mcp-live-status/status` |
| Network | None outbound. The browser half fetches only that local route. |
| Storage | None. No cache, no history, no files written. |

**On secrets.** MCP servers are routinely launched with credentials on the
command line (`--connectionString mongodb+srv://user:pass@host`, `--token …`).
Because the status payload is fetched by a browser, argument values are
filtered before they leave the host: anything containing a URI scheme or
`user:pass@` userinfo, anything following a flag named like a secret
(`pass`/`token`/`key`/`secret`/`auth`/`conn`/`dsn`/`uri`/`url`), any opaque blob
over 24 characters, and anything over 40 characters is dropped. `env` is never
read at all. HTTP transports keep only `origin + pathname`, so query strings and
userinfo never ship.

The result is recognisable but not exploitable — `npx -y mongodb-mcp-server
--readOnly` rather than the connection string.

This filter is heuristic. If you run a server whose *plain* arguments are
themselves sensitive, set `showDisabled: false` and review what the route
returns before exposing DSH beyond localhost:

```bash
curl 127.0.0.1:3080/dsh-mcp-live-status/status
```

## Compatibility

- DeepSeek Harness `0.1.0-rc.7` (developed and verified against this version)
- Profile: `web` (the plugin targets the browser UI; `platform: web`)
- Requires `webServer`. `loader` and `tools` are read opportunistically through
  `ctx.reflect.get()` — a profile missing either still boots, it just reports
  less.
- No dependency on `dsh-typert-loader` or `dsh-api-gateway`.

## Known limitations

- **Needs a session.** `conversation.input.left` is session-scoped and the shell
  passes it no zone until a session exists. In practice the pill is visible from
  the new-session screen onward, because picking a workspace creates the
  session — but on a truly session-less screen it renders nothing.
- **Polling, not push.** mcp-client emits no status events, so there is nothing
  to subscribe to. The readout can be up to one interval stale.
- **`no-tools` is inference, not a probe.** A server that genuinely publishes
  zero tools is indistinguishable from one that never connected. Both are
  amber. The plugin never opens its own MCP connection to check.
- **Counts tools, not health.** A server that connected and then went silent
  keeps its last registered tool generation until mcp-client's reconnect budget
  runs out.

## Development

```bash
npm install
npm run build          # tsc — host half only; the browser half is plain JS
dsh plugin --profile web add ./dsh-mcp-live-status
dsh --profile web --dump-config | grep -A2 dsh-mcp-live-status
```

`dist/` is committed on purpose. pnpm blocks a git dependency's `prepare`
script by default, and the `allowBuilds` key it asks for is pinned to the commit
hash — so building on install would make every user paste a new key on every
release. Run `npm run build` before committing a change to `src/`.

The two halves are independent:

- `src/index.ts` → `dist/index.js` — Node side; collects status, serves the route.
- `lib/client.js` — browser side; hand-written CJS factory, no build step, no
  JSX. The module loader discovers it from `exports["./client"]` +
  `dsh.client` and serves it at `/plugins/dsh-mcp-live-status/client.js`.

To reproduce the amber state, add a server pointing at a command that does not
exist and leave `failOnStartupError: false`.

## License

MIT

Install

dsh plugin --profile web add github:felix-lj-ct/dsh-mcp-live-status#4023f341ff6d5af8744fe91a2673bd9cf99022e6

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
Source