deepseek-harness
deepseek-ai
DeepSeek Harness: Everything is a Plugin.
PROJECT TOPICS
INSTALL REFERENCE
dsh plugin --profile web add github:GCS-ZHN/mcp-sentinel
该命令指向仓库当前默认分支;尚无绑定当前 commit 的完整验证结果。
PROJECT README
A sentinel between an AI agent and MCP servers — polling long-running tasks on the agent's behalf so that token-costly status loops never enter the LLM inference path.
This is a monorepo: a harness-agnostic core (@gcszhn/mcp-sentinel-core) plus one thin plugin package per agent host.
Zero MCP re-configuration. The sentinel never asks the user to configure MCP servers of its own. Installing the plugin is the whole setup — it discovers and reuses the MCP servers the harness already has, in whichever way that harness exposes them:
client.config.get().mcp and hands the resolved servers to the core, which
owns the connection lifecycle. No extra MCP setup.mcp__<server>__<tool> tools already registered by
@deepseek-ai/dsh-mcp-client via ctx.tools.execute. No extra MCP setup.mcp-sentinel mcp --harness <codex|opencode|custom> is a plain stdio MCP server that discovers the MCP
servers the harness already exposes (codex mcp list --json, opencode debug config, or a --mcp-config file) and skips its own entry. No message
notification channel — agents collect results with attach/status/read.The agent immediately sees the MCP servers it already configured for that harness; there is no sentinel-specific MCP config, mock server, or demo wiring to maintain.
Install instructions and per-harness details live in each plugin's own README.
| Harness | Plugin package | Docs |
|---|---|---|
| OpenCode | @gcszhn/mcp-sentinel-opencode-plugin |
README |
| DeepSeek Harness | @gcszhn/mcp-sentinel-deepseek-harness-plugin |
README |
| Any harness (CLI) | @gcszhn/mcp-sentinel-cli |
README |
The shared core ships separately as @gcszhn/mcp-sentinel-core — see its README.
When an agent submits a long-running job through an MCP tool, it must repeatedly call the server to check progress — each round-trip burns context window tokens.
sequenceDiagram
participant A as Agent (LLM)
participant M as MCP Server
Note over A: Without sentinel
A->>M: check status
M-->>A: running...
Note over A: token cost 💸
A->>M: check status
M-->>A: running...
Note over A: token cost 💸
A->>M: check status
M-->>A: completed ✓
Note over A: token cost 💸
mcp-sentinel moves the polling loop out of the agent and into the plugin runtime — 2 inference calls regardless of task duration.
sequenceDiagram
participant A as Agent (LLM)
participant S as Sentinel Plugin
participant M as MCP Server
A->>S: poll_mcp(server, tool, until)
Note over A: token cost 💸 (once)
loop silent polling (zero tokens)
S->>M: call tool
M-->>S: running...
S->>S: evaluate condition
end
S->>M: call tool
M-->>S: completed ✓
S->>A: promptAsync(result)
Note over A: token cost 💸 (once)
Environment variables for controlling memory usage:
| Variable | Default | Description |
|---|---|---|
SENTINEL_MAX_POLL_LOG |
unlimited | Max poll log entries per task (FIFO trim) |
SENTINEL_TASK_TTL_MS |
unlimited | Auto-cleanup completed tasks after N milliseconds |
Both accept positive integers only. Zero, negative, or non-numeric values are treated as unlimited/disabled.
mcp_sentinel_pollSubmit a long-running MCP tool call and poll it at regular intervals until a condition is met. The sentinel polls silently (zero token cost) and notifies you when done.
| Parameter | Type | Default | Description |
|---|---|---|---|
server |
string | required | MCP server name (resolved from the host's MCP config) |
tool |
string | required | Tool name to call on the server |
args |
object | {} |
JSON object of arguments for the tool |
interval |
number | 5000 |
Poll interval in milliseconds |
timeout |
number | optional | Max poll duration in ms (unset = no limit) |
until |
object | required | JSON condition object |
Returns a sentinel ID immediately. The agent is notified when done (the delivery mechanism is host-specific).
args and until are native JSON values in the tool arguments — not JSON
strings. interval is clamped to a minimum of 1000 ms; a positive timeout is
clamped to a minimum of 5000 ms (values below the floor are raised).
mcp_sentinel_statusCheck the status of sentinel tasks, list active tasks, or cancel a running task.
| Parameter | Type | Description |
|---|---|---|
action |
"status" | "list" | "cancel" |
Action to perform |
id |
string | Sentinel ID (required for status and cancel) |
mcp_sentinel_attachBlock the agent, waiting for a sentinel task to complete. Sleeps and checks status internally with zero token cost. If interrupted by harness, the background async notification still fires normally.
| Parameter | Type | Default | Description |
|---|---|---|---|
id |
string | required | Sentinel ID to wait for |
timeout |
number | optional | Max wait time in ms (unset = wait indefinitely) |
mcp_sentinel_readRead raw poll outputs from a sentinel task. Useful for debugging when a condition isn't matching — inspect actual MCP responses. Supports range-based pagination via offset.
| Parameter | Type | Default | Description |
|---|---|---|---|
id |
string | required | Sentinel ID to read outputs from |
offset |
number | end-N |
0-based start index (default: from end) |
limit |
number | 5 |
Max number of outputs to return |
Conditions are pure declarative data — no executable code, no injection surface.
// Simple comparison
{ "path": "status", "is": "eq", "value": "completed" }
// Array index access
{ "path": "[0].data.path", "is": "eq", "value": "found" }
// Regex match
{ "path": "log", "is": "match", "value": "^error" }
// Logical composition
{
"and": [
{ "path": "status", "is": "eq", "value": "completed" },
{ "path": "tasks[0].exit_code", "is": "eq", "value": 0 }
]
}
| Operator | Description |
|---|---|
eq |
Strict equality |
ne |
Not equal |
gt |
Greater than (numeric) |
gte |
Greater than or equal |
lt |
Less than |
lte |
Less than or equal |
contains |
String contains |
match |
Regex match (new RegExp(value).test(data)) |
| Combinator | Description |
|---|---|
{ "not": <condition> } |
Negation |
{ "and": [...] } |
All must match |
{ "or": [...] } |
Any must match |
Uses property-access notation with array index support:
status → obj.status
tasks[0].exit_code → obj.tasks[0].exit_code
[0].data.path → obj[0].data.path
items[2].name → obj.items[2].name
The project is a monorepo where each layer ships as its own npm package. The core knows nothing about any host; every harness is a thin, self-contained package layered on top of it.
| Package | Purpose | Published as |
|---|---|---|
packages/core |
sentinel engine, tool handlers, condition evaluator, connection pool, env, logger, types | @gcszhn/mcp-sentinel-core |
packages/opencode |
OpenCode adapter: tool() definitions + client.config.get() + session.promptAsync |
@gcszhn/mcp-sentinel-opencode-plugin |
packages/deepseek-harness |
DeepSeek Harness adapter: external-invoker mode via ctx.tools.execute + Agent.followup |
@gcszhn/mcp-sentinel-deepseek-harness-plugin |
packages/cli |
harness-agnostic MCP stdio CLI (mcp-sentinel mcp --harness …) |
@gcszhn/mcp-sentinel-cli |
packages/<harness> (future) |
one entry per host, e.g. claude-code |
@gcszhn/mcp-sentinel-<harness>-plugin |
The core exposes one uniform seam — ToolInvoker, a
(server, tool, args) => Promise<unknown> function the engine calls once per
poll — so each harness can plug in its own MCP access strategy without the core
knowing which host it is running under.
// core — the uniform interface (harness-agnostic)
type ToolInvoker = (server: string, tool: string, args: Record<string, unknown>) => Promise<unknown>;
// the core engine accepts an invoker instead of reading host config itself
startSentinel(request, invoke: ToolInvoker): Promise<string>;
There are two ways to build the invoker:
McpConfig, builds a ServerResolver with makeServerResolver, and
wraps it in makeConnectionInvoker, letting the core own the connection
lifecycle.(server, tool, args) => result function and the core never opens a
connection.MCP config discovery is the harness's job — different hosts fetch it
differently (OpenCode via client.config.get().data mcp.* flat keys, Codex
via codex mcp list --json, a --mcp-config file, …), and external-invoker
hosts skip config discovery entirely.
The core's second seam is the notifier: a harness installs a completion
callback with setNotifier(task, event), delivered through the host's message
channel (OpenCode promptAsync, DeepSeek Harness Agent.followup). The core
has no opinion on how a notification is rendered or pushed.
AGENTS.md.packages/<harness>/package.json named @gcszhn/mcp-sentinel-<harness>-plugin
with a dependency on @gcszhn/mcp-sentinel-core.ToolInvoker: either parse the host's MCP config into a McpConfig
and wrap it with makeConnectionInvoker(makeServerResolver(...))
(connection-pool mode), or pass a host-owned (server, tool, args) => result
function (external-invoker mode).handlePoll /
handleStatus / handleAttach / handleRead handlers.setNotifier, pushing completions through the
host's message channel (e.g. OpenCode promptAsync, DeepSeek Harness
Agent.followup).sequenceDiagram
participant A as Agent (any host)
participant H as Harness adapter
participant C as Core engine
participant M as MCP Server
A->>H: poll(server, tool, until)
H->>H: resolveServer()
H->>C: startSentinel(...)
C-->>H: sentinel ID
H-->>A: acknowledgment
loop every interval ms (zero tokens)
C->>M: call tool(args)
M-->>C: response
C->>C: evaluateCondition(until, response)
end
C->>H: notify(completed)
H->>A: host-specific completion push
packages/
core/ # @gcszhn/mcp-sentinel-core (zero host deps)
src/
engine.ts # startSentinel / cancel / getTask / getActive / cleanup
tools.ts # handlePoll / handleStatus / handleAttach / handleRead
condition.ts # condition evaluator
connection-pool.ts # MCP client pool (@modelcontextprotocol/sdk)
env.ts # SENTINEL_* env
logger.ts # pluggable sink
resolver.ts # makeServerResolver (McpConfig → ServerResolver)
types.ts # McpServerConfig / ServerResolver / Sentinel*
index.ts # public barrel
tests/
opencode/ # @gcszhn/mcp-sentinel-opencode-plugin
src/
plugin.ts # PluginModule entry
index.ts # tool() definitions + promptAsync notifier
config.ts # parseOpencodeMcpConfig (opencode `mcp` block) → McpConfig
tests/
deepseek-harness/ # @gcszhn/mcp-sentinel-deepseek-harness-plugin
src/
index.ts # external-invoker mode: ctx.tools.execute + Agent.followup
tests/
cli/ # @gcszhn/mcp-sentinel-cli (harness-agnostic stdio MCP server)
src/
cli.ts # CLI entry: mcp-sentinel mcp --harness <codex|opencode|custom|none>
mcp-server.ts # registers the 4 tools; connection-pool mode; no notifier
config.ts # codex mcp list / opencode debug config / --mcp-config discovery
schema/ # mcp-config.schema.json (ships in the npm tarball)
tests/
# future harnesses, one concrete package each:
# claude-code/ ...
Build order: each package builds independently, but the adapters type-check and run against
@gcszhn/mcp-sentinel-core's publisheddist/. Runbun run build(core first, then opencode, deepseek-harness, cli) beforebun test.
MIT
CLASSIFICATION EVIDENCE
系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。当前命中: 无有效分类标签。