Skills Plugins MCP Prompt Model 导航 博客 资讯 我的中心
模型与提供方 #deepseek-harness#dsh#dsh-plugin#mcp#model-context-protocol#tools

dsh-mcp-workspace-scope

按会话所在目录限定可用的 MCP 服务器:隐藏工具并拒绝调用,输入框可按会话临时增减,并显示各服务器真实运行状态

felix-lj-ct @felix-lj-ct ⬇ 2 ★ 2 main

安装

dsh plugin --profile web add github:felix-lj-ct/dsh-mcp-workspace-scope
下载安装清单

需要可复现安装时,可在仓库后追加 #commit 固定提交。

按会话所在目录限定可用的 MCP 服务器:隐藏工具并拒绝调用,输入框可按会话临时增减,并显示各服务器真实运行状态

该插件未提供要点说明,请参考仓库 README。

deepseek-harnessdshdsh-pluginmcpmodel-context-protocoltools
  1. 安装并启动 DeepSeek Harness:npx @deepseek-ai/dsh web
  2. 在终端执行上面的安装命令(CLI 会解析插件并核验来源)
  3. 用 dsh plugins list 确认已安装,必要时重启 Harness 生效

插件以当前 dsh 进程的权限运行,安装时可能执行代码。请先通读仓库源码与许可证,确认无破坏性命令与越权访问;本站只做索引,不对第三方插件安全性作担保。

代码仓库github.com/felix-lj-ct/dsh-mcp-workspace-scope
许可证MIT
主要语言main
下载量2
GitHub 星标2
最近推送2026-08-31
收录日期2026-09-19
分类模型与提供方

事实信息来自公开插件目录快照(2026-10-01),介绍文案由本站再加工。

以下为插件仓库 README 全文(原始内容,由公开目录抓取整理)。

# dsh-mcp-workspace-scope

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-workspace-scope)
[![npm](https://img.shields.io/npm/v/dsh-mcp-workspace-scope)](https://www.npmjs.com/package/dsh-mcp-workspace-scope)
[![license](https://img.shields.io/npm/l/dsh-mcp-workspace-scope)](LICENSE)

**Give every project only the MCP servers it actually needs — and flip one on for a single
session when it doesn't.**

A DeepSeek Harness plugin that scopes MCP tool injection by the directory a session was
opened in, with per-session switches in the composer for the exceptions.

![The MCP scope pill in the composer, with a per-server switch for this session](docs/screenshot.png)

## Features

- Per-directory allow-list of MCP servers, inherited by subdirectories
- Hides the tools **and** refuses the calls — context savings plus a hard boundary
- Per-session switches right in the composer: narrow or widen the session you are in,
  temporarily, without touching the rules
- Honest readout: each server's runtime state, so "allowed but dead" is visible
- Settings-page editor; a saved rule reaches running sessions immediately

## Why

A profile accumulates MCP servers. Every one of them ships its whole tool list into
**every** session, because MCP is process-global in DSH: `@deepseek-ai/dsh-mcp-client`
registers on the root `ctx.tools` as `mcp____`, so a session that
will only ever touch Jira still carries three database servers and a browser driver in
its context window — and can call them by mistake.

This plugin narrows that per directory. A session opened in `D:\work\proj-a` gets only
`atlassian`, one opened in `D:\work\proj-b` gets only `playwright`, and every other
folder is left exactly as it was. When the exception comes up — "I need bigquery for the
next ten minutes" — the composer pill is a switch, for that session only.

## Limits

- **Cannot conjure a disabled server.** Anything in an allow-list must already be enabled
  in the profile (e.g. via `dsh-skill-mcp-panel`). This plugin only subtracts.
- **Saves no processes:** a hidden server still runs and still holds its memory. For
  "don't even start it", move the MCP rows into an agent preset instead — a different
  approach.
- **Subagents are scoped independently**, from their own working directory rather than by
  inheriting the parent's restriction (see [How it works](#how-it-works)).

## Install

```bash
dsh plugin --profile web add dsh-mcp-workspace-scope
```

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

```bash
dsh plugin --profile web add github:felix-lj-ct/dsh-mcp-workspace-scope
```

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

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

The bundle layer in `cordis.patch.yml` mounts the host half; no manual profile edit. And
nothing changes yet: with no rules document, every session still injects every MCP server
(see below).

## Rules document

Default path: `~/.dsh/mcp-workspace-scope.json` (follows `$DSH_HOME` when set).
**No file = no effect** — every session keeps injecting all MCP servers, so installing
the plugin changes nothing until you write rules.

```json
{
  "default": "*",
  "rules": [
    {
      "path": "D:/work/master-data-management",
      "servers": ["atlassian", "bigquery"]
    },
    {
      "path": "D:/work/frontend",
      "servers": ["playwright", "context7"]
    },
    {
      "path": "D:/scratch",
      "servers": []
    }
  ]
}
```

| Field | Value | Meaning |
| --- | --- | --- |
| `default` | `"*"` | Unmatched folders inject everything (the default, and a no-op) |
| `default` | `[]` | Unmatched folders inject no MCP server at all |
| `default` | `["a","b"]` | Unmatched folders inject only those |
| `rules[].path` | directory | `~/` and `$DSH_HOME` expand; either slash works; case-insensitive on Windows |
| `rules[].servers` | as `default` | Allow-list for that directory and everything under it |

Matching:

- Subdirectories **inherit** their parent rule; comparison happens at a separator
  boundary, so `/ws/proj` never matches `/ws/project`.
- **Longest path wins** — set a baseline on `/ws` and override it on `/ws/proj`.
- Among equal-length duplicates the **later row wins**.
- A session with no cwd falls back to `default`.
- A rules change applies to **running sessions immediately** (saving from the settings
  page, or editing the file directly, both trigger it). This is deliberate: a workspace
  has one reusable blank session and the harness declines to open a second one while it
  is unused, so "add the workspace, set its scope, start working" needs the rule to reach
  the session you are looking at. The harness already behaves this way — disabling a
  server on the MCP page unregisters its tools from every live session through HMR. Set
  `applyToRunningSessions` to `false` for the old freeze-at-creation behaviour.

## Plugin config (optional)

Only needed to relocate the document or change the failure policy. Goes under `config:`
on this plugin's row in the profile's `cordis.patch.yml`:

| Key | Default | Meaning |
| --- | --- | --- |
| `rulesPath` | `""` | Rules document path; empty = `/mcp-workspace-scope.json` |
| `enforceGuard` | `true` | Also refuse the call, not just hide the tool. Keep this on (see below) |
| `onRulesError` | `"open"` | Unreadable/malformed document: `open` = inject everything (as if absent), `closed` = inject nothing |
| `applyToRunningSessions` | `true` | Re-scope running sessions when the rules change; `false` freezes each session at creation |
| `logDecisions` | `true` | One log line per session: which rule matched, which servers survived |

## How it works

```
session created in some directory
      ↓ agent/created
read session.header.cwd → longest-prefix rule match → allow-list
      ↓
agent.ctx.tools.restrict({ deny: [...mcp__* outside the allow-list] })   ← hide
agent.ctx.tools.guard(...)                                              ← refuse calls
      ↓ tools/change (server connected, reconnected, unloaded)
recompute the deny set and reapply
```

The two mechanisms are not redundant — they fire at different times:

- `restrict()` **must** be called on an agent-scoped context (the runtime rejects a
  root-context call by design: it would mask every agent), and it validates every name
  against the tools that scope currently inherits — so a deny set **cannot be written
  ahead of a server that has not connected yet**. Visibility keeps up by recomputing on
  `tools/change`.
- `guard()` is evaluated at call time and validates nothing up front, so it is immune to
  the "registered a moment ago, called immediately" gap.

One known boundary: **subagents do not inherit the parent's restriction.**
`agentPresets.composeFrom()` binds a child agent's scope parent to the preset's standing
scope rather than to the parent agent, so a parent's `restrict()` never reaches it. This
plugin evaluates subagents independently from their own `session.header.cwd` (normally
inherited from the parent, so the outcome matches).

## UI

Installed, the plugin shows up in two places:

**1. Settings → "MCP scope"** (right under the MCP page)

![The rules editor: a default row, one directory denied every server, one with a custom pick](docs/settings.png)

- Header: rules-document path, failure policy, whether calls are refused; says so when
  the document does not exist yet.
- **Default (directories matching no rule)**: All / None / Custom, with a server picker
  for Custom.
- **Directory rules**: one editable row each; the picker lists every MCP server in the
  profile with its live tool count and marks disabled ones (still selectable — they just
  yield no tools).
- Add a rule from the known-workspace dropdown, or type a path by hand.
- Saving writes the document atomically host-side; a rejected payload shows the reason
  verbatim and leaves the file untouched.
- Saving re-scopes running sessions immediately (with `applyToRunningSessions: false` a
  new session is needed, and the badge then marks the session as frozen and names what
  the current rules would give).

**2. An MCP badge in the composer tool row**

Rendered **always**, matched rule or not — deliberately: a capability readout that
disappears when nothing is configured cannot tell you whether scoping is in effect.

- no rule matched → `MCP all`
- rule matched → `MCP atlassian` (or `atlassian +1` for several)
- rule with `[]` → `MCP none` (amber)

The popover shows the session directory, which rule decided it, **each server's runtime
state**, and the visible/hidden tool counts — measured from that session's own agent
scope, not predicted from the rules (a session with no running agent is labelled as a
prediction).

### Changing one session's scope from the popover

Every server row in that popover carries a **toggle switch**, and the whole row is the hit
area — flip one to add or remove that server for the session you are looking at, or use
**All** / **None** / **Back to the rule**. The response to the write is the new readout,
so what is drawn is always what the host installed.

- **Temporary and in-memory.** Nothing is written to the rules document, and the override
  dies with the agent — a new session (and the host after a restart) is back on the rules.
- **It outranks the rules while set.** A rules save no longer disturbs that session, so a
  switch you flipped a moment ago is not silently undone. "Back to the rule" re-joins.
- **It may widen, not just narrow** — up to whatever the profile has enabled. The case
  this exists for is "I need bigquery for the next ten minutes", which a narrow-only
  control cannot serve. It still cannot conjure a disabled server (`restrict()` only
  subtracts, so the enabled set is a hard ceiling).
- The badge turns **blue with a `*`** while a session is overridden: not a warning, but
  the settings page no longer describes this session.

A session with no running agent has no scope to restrict, so the rows stay inert there
and the host refuses the write (`400`) rather than reporting a scope the model never got.

### "Allowed but unusable"

A rule may name four servers with two of them disabled in the profile: the scope looks
right and the session still cannot do the work. So every server carries a state dot (the
join is borrowed from `dsh-mcp-live-status`, same author, MIT):

| State | Meaning |
| --- | --- |
| connected | mounted and registering tools — the only genuinely usable state |
| up, not connected | fiber is ACTIVE but no tool registered (handshake never completed) |
| starting / mount failed / not mounted / disabled | the remaining cases |

Why the tool evidence is required: `dsh-mcp-client` defaults to
`failOnStartupError: false`, so **a server whose transport never connected still reaches
fiber state ACTIVE** — mount phase alone cannot tell "connected" from "up but dead",
while mcp-client registers nothing until connect() and listTools() both succeed.

The badge therefore turns amber with a `•` when an allowed server cannot serve anything,
and red with a `!` when a rule names a server this profile does not have (a typo, or a
removed server).

This also fixed a quiet attribution bug: `serverName` may contain underscores, so `foo`
and `foo__bar` can coexist and `mcp__foo__bar__baz` is legal for either — splitting at
the first `__` hands it to `foo` and mis-scopes it in both directions. Ownership now goes
to the longest matching name (with a test to keep it that way).

Note how this differs from `dsh-mcp-live-status`: that plugin reads the **global** view
(which server processes are connected), so it always lists every enabled server; this one
layers "what may this session use" on top. They measure different things, do not conflict,
and this plugin does not depend on it.

## Permissions and risk

This plugin **takes capabilities away** from a session; it can never add one. Everything
it grants must already be enabled in the profile, and managing servers stays with the
settings page — there is no way here to start, stop or reconfigure one.

| Surface | What it does |
|---|---|
| `ctx.tools` | Reads registered tool *names*; installs a per-agent `restrict()` + `guard()`. Never calls a tool. |
| `ctx.loader` | Reads the configured plugin tree (read-only iteration) to list MCP servers |
| `ctx.reflect` | Optional reads of `sessions` and `workspaceRegistry` — session cwd and known workspace paths, for the readout and the path picker |
| `ctx.webServer` | Three local JSON routes under `/dsh-mcp-workspace-scope`: read state, read one session's scope, write rules or a session override |
| Network | None outbound. The browser half fetches only those local routes. |
| Storage | One file: the rules document (`~/.dsh/mcp-workspace-scope.json` by default), written atomically, only when you press Save. |

**The failure mode to know about** is a rule that is stricter than you meant: the session
quietly has fewer tools, and the model will say it cannot do the thing rather than that it
is not allowed to. That is what the composer pill exists for — it states what the session
actually got, measured from the agent's own view. A broken rules document fails **open**
by default (`onRulesError`), so a typo cannot strip a working session; set it to `closed`
if you would rather it deny everything.

**No secrets are read.** The plugin only ever handles server *names* and tool *names* —
never an MCP server's command line, arguments, or environment.

## Development

```bash
npm install
npm run build     # tsc → dist/ (committed; see .gitignore for why)
npm test          # 24 smoke cases against a fake harness — no DSH required
```

The smoke test reproduces the three `ToolRuntime` behaviours this plugin leans on (the
global view ignores scoped restrictions, `restrict()` throws on unknown names, and both
`restrict()` and its disposer emit `tools/change`). Getting any of them wrong fails
silently in production. It also fakes `webServer`, so the JSON routes — including the
per-session override — are driven end to end, and one case runs headless to prove the
gating never depends on them.

## License

MIT

数据来源:公开的 DeepSeek Harness 插件目录与各插件 GitHub 仓库。本站为独立第三方目录,与 DeepSeek、幻方(High-Flyer)及插件作者均无隶属或背书关系。

每日精选 Skill 推荐,免费送到你邮箱

输入邮箱,每天接收一个精选 AI Agent 技能推荐。完全免费,持续更新。

提交后我们会发送一封确认邮件,点击邮件里的链接才会开始收信。

完全免费,取消任意时间。我们不会发送垃圾邮件。