@nones/dsh-session-mcp 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +151 -0
- package/README.md +87 -38
- package/dist/tools.js +134 -0
- package/dist/types.d.ts +78 -3
- package/package.json +5 -2
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for looking. This is a small, focused package: an MCP endpoint for one DeepSeek
|
|
4
|
+
Harness instance, plus the settings page that hands out its config. The rules below are
|
|
5
|
+
mostly about keeping it that way.
|
|
6
|
+
|
|
7
|
+
## What this repo is
|
|
8
|
+
|
|
9
|
+
A **DeepSeek Harness plugin bundle** with two halves:
|
|
10
|
+
|
|
11
|
+
| Half | Runs in | Entry |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| Host | the harness process | `src/index.ts` → `dist/index.js` |
|
|
14
|
+
| Client | the Web UI (browser) | `src/client.ts` → `dist/client.js` |
|
|
15
|
+
|
|
16
|
+
The Host half owns the HTTP listener, the bearer gate, the tools and the config route. The
|
|
17
|
+
Client half owns the Settings page. They talk over the authenticated `/api` channel.
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
- Node ≥ 20
|
|
22
|
+
- [bun](https://bun.sh) (dependency install, scripts)
|
|
23
|
+
- A DeepSeek Harness installation to try the plugin in (optional for the smoke test)
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
git clone https://github.com/ZeiZel/dsh-session-mcp.git
|
|
29
|
+
cd dsh-session-mcp
|
|
30
|
+
bun install
|
|
31
|
+
bun run typecheck # tsc --noEmit for both targets
|
|
32
|
+
bun run test # smoke test, no harness needed
|
|
33
|
+
bun run build # emits dist/
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`dist/` is committed. Rebuild and commit it whenever you change anything under `src/` —
|
|
37
|
+
installs from git or from a directory must not need a build step.
|
|
38
|
+
|
|
39
|
+
## Layout
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
src/index.ts Cordis plugin: bearer, listener, state file, config route
|
|
43
|
+
src/server.ts node:http listener + bearer gate + official MCP handler
|
|
44
|
+
src/tools.ts the tool surface, backed by ctx.sessionController
|
|
45
|
+
src/log.ts bounded journal (last five calls)
|
|
46
|
+
src/config.ts token persistence, snippets, payload types
|
|
47
|
+
src/client.ts the Settings page (classic script)
|
|
48
|
+
src/types.ts the structural slice of Harness types we depend on
|
|
49
|
+
test/smoke.mjs endpoint + every tool + journal, against a stub controller
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Why two tsconfigs
|
|
53
|
+
|
|
54
|
+
`tsconfig.json` builds the Node half. `tsconfig.client.json` builds only `src/client.ts`
|
|
55
|
+
with `verbatimModuleSyntax: false`, because the Web shell loads that file as a **classic
|
|
56
|
+
script** through `window.__ModuleLoader__`. A single config emits `export {}` at the end,
|
|
57
|
+
which is a syntax error in a classic script. If you add another Client file, add it to the
|
|
58
|
+
second config's `include` and to `package.json` `exports`.
|
|
59
|
+
|
|
60
|
+
## Trying it in a harness
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
dsh plugin --profile desktop add /absolute/path/to/dsh-session-mcp
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Then:
|
|
67
|
+
|
|
68
|
+
- **Host half** — live immediately. The endpoint starts, the state file is written, the
|
|
69
|
+
config route answers. `$DSH_HOME/state/dsh-session-mcp.json` has the URL and token.
|
|
70
|
+
- **Client half** — needs a **full harness restart**. The browser module roster is composed
|
|
71
|
+
from the packages present at boot, and the manifest resolution is cached for the life of
|
|
72
|
+
the process. Reloading the window is not enough.
|
|
73
|
+
|
|
74
|
+
Useful checks while iterating:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# endpoint up? (401 without the token is the healthy answer)
|
|
78
|
+
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:7801/mcp \
|
|
79
|
+
-H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \
|
|
80
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
|
|
81
|
+
|
|
82
|
+
# what the Settings page would render
|
|
83
|
+
cat "$DSH_HOME/state/dsh-session-mcp.json"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
To remove it again: `plugin_manager` `remove_bundle`, or `dsh plugin --profile desktop remove`.
|
|
87
|
+
|
|
88
|
+
## Adding a tool
|
|
89
|
+
|
|
90
|
+
1. Add the method you need to `SessionController` in `src/types.ts` (structural types only —
|
|
91
|
+
do not import Harness packages, this package must stay installable on its own).
|
|
92
|
+
2. Register it in `src/tools.ts` inside `registerTools`, wrapped in `tracked('<name>', …)`
|
|
93
|
+
so the Settings journal records it. Pass `true` as the third `tracked` argument when the
|
|
94
|
+
addressed conversation comes from the **result** rather than the arguments
|
|
95
|
+
(`session_create` does this).
|
|
96
|
+
3. Return `ok(value)` on success and `fail(error)` on failure — never throw past the
|
|
97
|
+
handler, and never return an error without `isError`.
|
|
98
|
+
4. Add the tool to `EXPECTED_TOOLS` and to the `calls` table in `test/smoke.mjs`.
|
|
99
|
+
5. Add the stub method to the smoke controller.
|
|
100
|
+
6. Document it in the README tools tables.
|
|
101
|
+
7. `bun run typecheck && bun run test && bun run build`, commit `dist/`.
|
|
102
|
+
|
|
103
|
+
Keep descriptions written for a model: what the tool does, when to call it, what it costs.
|
|
104
|
+
|
|
105
|
+
## Changing the Settings page
|
|
106
|
+
|
|
107
|
+
The page registers into the `settings.section` slot (list; options `{ id, order, label }`)
|
|
108
|
+
and renders inside the settings content column. Conventions:
|
|
109
|
+
|
|
110
|
+
- Text goes through the Client locale service: register `en` and `ru` dictionaries with
|
|
111
|
+
`ctx.locale.register(NS, locale, dict)` and translate with `ctx.locale.bind(NS)`. Both
|
|
112
|
+
dictionaries must carry the same keys.
|
|
113
|
+
- Colors come from theme tokens (`--dsw-alias-*`), never hardcoded.
|
|
114
|
+
- Everything the page shows arrives from `/api/dsh-session-mcp/config`; do not add a second
|
|
115
|
+
endpoint.
|
|
116
|
+
- Clean up subscriptions and timers in the effect's cleanup function.
|
|
117
|
+
|
|
118
|
+
## Commit and pull request
|
|
119
|
+
|
|
120
|
+
- Conventional commits: `feat:`, `fix:`, `docs:`, `chore:`, `ci:`.
|
|
121
|
+
- One logical change per commit; `chore(release): x.y.z` for the version bump.
|
|
122
|
+
- A pull request should say what changed, why, and what you verified. `bun run test` must
|
|
123
|
+
pass and `dist/` must be current.
|
|
124
|
+
- The smoke test is the contract. If you change a tool's behaviour, change its assertion.
|
|
125
|
+
|
|
126
|
+
## Releasing
|
|
127
|
+
|
|
128
|
+
Publishing is automated. Never run `npm publish` by hand.
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
npm version patch --no-git-tag-version # or minor / major
|
|
132
|
+
git commit -am "chore(release): 1.3.1"
|
|
133
|
+
git tag v1.3.1
|
|
134
|
+
git push origin master --tags
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The `release` workflow then builds, publishes to npm with a signed provenance statement, and
|
|
138
|
+
cuts the GitHub release. It needs the `NPM_TOKEN` repository secret.
|
|
139
|
+
|
|
140
|
+
## Gotchas
|
|
141
|
+
|
|
142
|
+
- **`bun publish` does not work here.** It goes through npm's staged publishing, which
|
|
143
|
+
leaves a `0.0.0-stage` placeholder and rejects the real version until approved. The
|
|
144
|
+
workflow uses `npm publish --provenance` for that reason.
|
|
145
|
+
- **The Client half needs a restart**, not a window reload (see above).
|
|
146
|
+
- **`git push` can hang** on machines where `credential.helper=osxkeychain` waits for a
|
|
147
|
+
Keychain prompt that never appears. Work around it with an explicit token:
|
|
148
|
+
`git -c credential.helper= -c "url.https://x-access-token:$(gh auth token)@github.com/.insteadOf=https://github.com/" push`.
|
|
149
|
+
- **npm granular tokens cannot unpublish or deprecate-over-2FA.** Plan versions accordingly.
|
|
150
|
+
- **The endpoint is loopback-only and bearer-authenticated by design.** Do not add a proxy,
|
|
151
|
+
tunnel, or "convenience" no-auth mode.
|
package/README.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# @nones/dsh-session-mcp
|
|
2
2
|
|
|
3
3
|
MCP (Model Context Protocol) server for **DeepSeek Harness sessions** — lets Claude Code,
|
|
4
|
-
Claude Desktop, Codex, Cursor and any other MCP client
|
|
5
|
-
|
|
4
|
+
Claude Desktop, Codex, Cursor and any other MCP client work with the conversations running
|
|
5
|
+
in your harness: list them, read them, prompt them, branch them, stop them, retitle them,
|
|
6
|
+
switch their model.
|
|
6
7
|
|
|
7
8
|
Ships as a DeepSeek Harness plugin: installing the bundle starts a bearer-authenticated MCP
|
|
8
|
-
endpoint **together with the harness
|
|
9
|
-
|
|
9
|
+
endpoint **together with the harness**, publishes a copy-ready client config, and adds a
|
|
10
|
+
**Settings → MCP** page with live status, the last few requests and every snippet you need.
|
|
11
|
+
No sidecar process to babysit, no port to guess, no second writer on the session log.
|
|
10
12
|
|
|
11
13
|
```
|
|
12
14
|
┌──────────────────┐ Bearer token ┌──────────────────────────────┐
|
|
@@ -47,10 +49,65 @@ bun add github:ZeiZel/dsh-session-mcp
|
|
|
47
49
|
|
|
48
50
|
Restart the harness (or reload the profile) and the endpoint starts with it.
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
> The Host half (endpoint, tools, config route) is live as soon as the bundle installs. The
|
|
53
|
+
> **Client half** — the Settings page — is composed into the browser module roster at boot,
|
|
54
|
+
> so it appears after the next harness start.
|
|
51
55
|
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
## Tools
|
|
57
|
+
|
|
58
|
+
Eleven tools. Everything is scoped to this harness: no other instance is reachable.
|
|
59
|
+
|
|
60
|
+
### Conversations
|
|
61
|
+
|
|
62
|
+
| Tool | What it does |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `sessions_list` | Conversations with id, title, working directory, run state and token usage. Start here. |
|
|
65
|
+
| `session_read` | Durable history — user, assistant and tool messages — pageable back with `beforeSeq`. |
|
|
66
|
+
| `session_inspect` | Vitals of one conversation: title, run state, tokens, context pressure, context breakdown, pending queue, selected model, goal. |
|
|
67
|
+
| `session_search` | Full-text search. Only works when the harness enables the session-query index; otherwise the host reports it is disabled. |
|
|
68
|
+
|
|
69
|
+
### Acting on a conversation
|
|
70
|
+
|
|
71
|
+
| Tool | What it does |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `session_send` | Deliver a user message into a live conversation (`queue` waits for the current turn, `steer` interrupts into it) and start a model turn. |
|
|
74
|
+
| `session_create` | Open a new blank conversation, optionally in a given `cwd` or `agentPreset`. |
|
|
75
|
+
| `session_rename` | Set the conversation title; returns the committed title and event position. |
|
|
76
|
+
| `session_fork` | Copy a prefix into a new conversation — branch off an alternative approach without touching the original. Omit `atSeq` to fork at the latest completed turn. |
|
|
77
|
+
| `session_cancel` | Stop the turn a conversation is running. Pending inbox work is kept, so a queued message is not lost. |
|
|
78
|
+
|
|
79
|
+
### Models
|
|
80
|
+
|
|
81
|
+
| Tool | What it does |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `models_list` | Every reachable provider/model route, the reasoning efforts each accepts, and the deployment default. |
|
|
84
|
+
| `session_select_model` | Pin one provider/model route (and optional reasoning effort) to a conversation. |
|
|
85
|
+
|
|
86
|
+
### A typical loop
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
sessions_list → find the conversation
|
|
90
|
+
session_inspect → is it running? how full is the context?
|
|
91
|
+
session_send → queue the next instruction
|
|
92
|
+
session_read → check what the agent answered
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Branching instead of hijacking:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
session_fork { sessionId, atSeq } → new conversation id
|
|
99
|
+
session_send { sessionId: <new> } → try the alternative there
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Settings → MCP
|
|
103
|
+
|
|
104
|
+
The harness renders a dedicated settings page. It shows:
|
|
105
|
+
|
|
106
|
+
- **Status** — listening, uptime, total requests served, and the last failure when one happened
|
|
107
|
+
- **Recent requests** — the newest five tool calls: what ran, when, the conversation it
|
|
108
|
+
addressed (title, with the id on hover), and whether it succeeded, with duration
|
|
109
|
+
- the live endpoint and the bearer token (masked until you reveal it)
|
|
110
|
+
- three copy-ready blocks:
|
|
54
111
|
|
|
55
112
|
| Block | For |
|
|
56
113
|
|---|---|
|
|
@@ -58,7 +115,12 @@ token (masked until you reveal it), and three copy-ready blocks:
|
|
|
58
115
|
| Codex — `~/.codex/config.toml` | Codex CLI / IDE |
|
|
59
116
|
| Claude Code — CLI | One `claude mcp add …` command |
|
|
60
117
|
|
|
61
|
-
Each block has its own **Copy** button; the endpoint and the token have one too.
|
|
118
|
+
Each block has its own **Copy** button; the endpoint and the token have one too. The page
|
|
119
|
+
refreshes every 10 seconds while it is open.
|
|
120
|
+
|
|
121
|
+
Only the last five calls are kept — the plugin is not a log store, and schema-rejected
|
|
122
|
+
calls (wrong arguments) never reach a tool, so they are not journalled. Conversation titles
|
|
123
|
+
come from one `session/list` read per page load, so the journal itself stays cheap.
|
|
62
124
|
|
|
63
125
|
The same data is on disk at `$DSH_HOME/state/dsh-session-mcp.json`:
|
|
64
126
|
|
|
@@ -78,7 +140,7 @@ The same data is on disk at `$DSH_HOME/state/dsh-session-mcp.json`:
|
|
|
78
140
|
}
|
|
79
141
|
```
|
|
80
142
|
|
|
81
|
-
|
|
143
|
+
## Connect a client
|
|
82
144
|
|
|
83
145
|
**Claude Code**
|
|
84
146
|
|
|
@@ -114,16 +176,6 @@ Authorization = "Bearer <token>"
|
|
|
114
176
|
Older clients that only speak the 2025 revisions still work: the endpoint serves the modern
|
|
115
177
|
revision and falls back to stateless serving for 2025-era traffic.
|
|
116
178
|
|
|
117
|
-
## Tools
|
|
118
|
-
|
|
119
|
-
| Tool | Description |
|
|
120
|
-
|---|---|
|
|
121
|
-
| `sessions_list` | Conversations with id, title, working directory, running state, token usage |
|
|
122
|
-
| `session_read` | Durable history (user / assistant / tool), pageable back with `beforeSeq` |
|
|
123
|
-
| `session_send` | Deliver a user message into a conversation (`queue` or `steer`) |
|
|
124
|
-
| `session_create` | Open a new blank conversation and return its id |
|
|
125
|
-
| `session_search` | Full-text search — only when the harness enables the session-query index |
|
|
126
|
-
|
|
127
179
|
## Security
|
|
128
180
|
|
|
129
181
|
- Binds **loopback only** (`127.0.0.1`) and rejects non-loopback `Host` headers.
|
|
@@ -131,8 +183,10 @@ revision and falls back to stateless serving for 2025-era traffic.
|
|
|
131
183
|
- The token is 32 random bytes, generated once, stored `0600` at
|
|
132
184
|
`$DSH_HOME/state/dsh-session-mcp.json`, and reused across restarts so pasted client
|
|
133
185
|
configs keep working.
|
|
134
|
-
-
|
|
135
|
-
|
|
186
|
+
- The Settings page reads its data from `/api/dsh-session-mcp/config` over the
|
|
187
|
+
authenticated browser channel, so the token never enters the served HTML.
|
|
188
|
+
- **Rotate** by deleting the state file and restarting the harness. Every client then needs
|
|
189
|
+
the new token.
|
|
136
190
|
- Never expose this endpoint through a proxy or a tunnel: the bearer is the only thing
|
|
137
191
|
between a client and every conversation you have.
|
|
138
192
|
|
|
@@ -153,34 +207,29 @@ Written in TypeScript, compiled to `dist/` (committed, so installs need no build
|
|
|
153
207
|
|---|---|
|
|
154
208
|
| `src/index.ts` | Cordis plugin: reads/creates the bearer, starts the listener, publishes the state file, serves the Settings config route, closes the listener on unload |
|
|
155
209
|
| `src/server.ts` | `node:http` listener with the bearer gate in front of the official SDK's `createMcpHandler` + `toNodeHandler` |
|
|
156
|
-
| `src/tools.ts` | The
|
|
210
|
+
| `src/tools.ts` | The eleven tools, backed directly by `ctx.sessionController` |
|
|
211
|
+
| `src/log.ts` | Bounded journal of the last five calls |
|
|
157
212
|
| `src/config.ts` | Token persistence, constant-time bearer comparison, client snippets |
|
|
158
213
|
| `src/client.ts` | The **Settings → MCP** page (classic script for `window.__ModuleLoader__`) |
|
|
159
214
|
| `src/types.ts` | The narrow structural slice of Harness types this package needs |
|
|
160
215
|
|
|
161
|
-
|
|
162
|
-
channel, so the token never enters the served HTML.
|
|
163
|
-
|
|
164
|
-
> **After installing**, restart the harness once: the browser module roster is composed
|
|
165
|
-
> from the packages present at boot, so a newly installed Client half appears on the next
|
|
166
|
-
> start.
|
|
216
|
+
## Releases
|
|
167
217
|
|
|
168
|
-
|
|
218
|
+
Publishing is automatic: pushing a `v*` tag makes GitHub Actions build the package, publish
|
|
219
|
+
it to npm with a **signed provenance** statement, and cut a GitHub release.
|
|
169
220
|
|
|
170
221
|
```bash
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
222
|
+
npm version patch --no-git-tag-version
|
|
223
|
+
git commit -am "chore(release): 1.3.1"
|
|
224
|
+
git tag v1.3.1
|
|
225
|
+
git push origin master --tags
|
|
174
226
|
```
|
|
175
227
|
|
|
176
|
-
|
|
177
|
-
`@modelcontextprotocol/node`, `zod`. Dev: `typescript`, `@types/node`.
|
|
228
|
+
The workflow requires one repository secret, `NPM_TOKEN` (npm automation token).
|
|
178
229
|
|
|
179
|
-
|
|
180
|
-
`dsh plugin add <dir>` work without a build step; `prepublishOnly` rebuilds it for npm.
|
|
230
|
+
## Contributing
|
|
181
231
|
|
|
182
|
-
|
|
183
|
-
GitHub release.
|
|
232
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
184
233
|
|
|
185
234
|
## License
|
|
186
235
|
|
package/dist/tools.js
CHANGED
|
@@ -252,4 +252,138 @@ export function registerTools(server, controller, journal) {
|
|
|
252
252
|
return fail(error);
|
|
253
253
|
}
|
|
254
254
|
}));
|
|
255
|
+
server.registerTool('session_inspect', {
|
|
256
|
+
title: 'Inspect one conversation',
|
|
257
|
+
description: 'Vitals of one conversation: title, run state, token usage, context pressure, pending queue, selected model and goal. Cheaper than reading history when you only need its state.',
|
|
258
|
+
inputSchema: z.object({
|
|
259
|
+
sessionId: z.string().min(1).describe('Conversation id from sessions_list.'),
|
|
260
|
+
}),
|
|
261
|
+
}, tracked('session_inspect', async ({ sessionId }) => {
|
|
262
|
+
try {
|
|
263
|
+
const rows = await controller.list({}, never());
|
|
264
|
+
const row = (rows?.items ?? []).find((item) => item.sessionId === sessionId);
|
|
265
|
+
const projections = await controller.projections({ sessionId }, never());
|
|
266
|
+
if (projections === null)
|
|
267
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
268
|
+
const values = (projections.values ?? {});
|
|
269
|
+
return ok({
|
|
270
|
+
sessionId,
|
|
271
|
+
title: values.title ?? null,
|
|
272
|
+
running: row?.running === true,
|
|
273
|
+
agentAvailable: row?.agentAvailable === true,
|
|
274
|
+
cwd: row?.cwd ?? null,
|
|
275
|
+
tokens: values.tokenUsage ?? null,
|
|
276
|
+
contextPressure: values.contextPressure ?? null,
|
|
277
|
+
contextBreakdown: values.contextBreakdown ?? null,
|
|
278
|
+
sessionStats: values.sessionStats ?? null,
|
|
279
|
+
inbox: values.inbox ?? null,
|
|
280
|
+
model: values.modelSelection ?? null,
|
|
281
|
+
goal: values.goal ?? null,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
return fail(error);
|
|
286
|
+
}
|
|
287
|
+
}));
|
|
288
|
+
server.registerTool('session_rename', {
|
|
289
|
+
title: 'Rename a conversation',
|
|
290
|
+
description: 'Set the title of one conversation. Returns the normalized title and the event position that committed it.',
|
|
291
|
+
inputSchema: z.object({
|
|
292
|
+
sessionId: z.string().min(1).describe('Conversation id from sessions_list.'),
|
|
293
|
+
title: z.string().min(1).max(200).describe('New title.'),
|
|
294
|
+
}),
|
|
295
|
+
}, tracked('session_rename', async ({ sessionId, title }) => {
|
|
296
|
+
try {
|
|
297
|
+
const value = await controller.rename({ sessionId, title });
|
|
298
|
+
return ok({ sessionId, title: value?.title ?? title, seq: value?.seq ?? null });
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
return fail(error);
|
|
302
|
+
}
|
|
303
|
+
}));
|
|
304
|
+
server.registerTool('session_fork', {
|
|
305
|
+
title: 'Fork a conversation',
|
|
306
|
+
description: 'Copy a conversation prefix into a new conversation and return its id. Omit atSeq to fork at the latest completed turn — useful to branch off an alternative approach without disturbing the original.',
|
|
307
|
+
inputSchema: z.object({
|
|
308
|
+
sessionId: z.string().min(1).describe('Conversation id from sessions_list.'),
|
|
309
|
+
atSeq: z.number().int().min(0).optional().describe('Exact inclusive source event seq to cut at.'),
|
|
310
|
+
}),
|
|
311
|
+
}, tracked('session_fork', async ({ sessionId, atSeq }) => {
|
|
312
|
+
try {
|
|
313
|
+
const request = { sessionId };
|
|
314
|
+
if (atSeq !== undefined)
|
|
315
|
+
request.atSeq = atSeq;
|
|
316
|
+
const value = await controller.fork(request);
|
|
317
|
+
return ok({ sourceSessionId: sessionId, sessionId: value?.sessionId ?? null });
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
return fail(error);
|
|
321
|
+
}
|
|
322
|
+
}));
|
|
323
|
+
server.registerTool('session_cancel', {
|
|
324
|
+
title: 'Stop a running turn',
|
|
325
|
+
description: 'Ask the Host to cancel the turn a conversation is currently running. Pending inbox work is retained, so a queued message is not lost.',
|
|
326
|
+
inputSchema: z.object({
|
|
327
|
+
sessionId: z.string().min(1).describe('Conversation id from sessions_list.'),
|
|
328
|
+
}),
|
|
329
|
+
}, tracked('session_cancel', async ({ sessionId }) => {
|
|
330
|
+
try {
|
|
331
|
+
const value = await controller.cancel({ sessionId });
|
|
332
|
+
return ok({ sessionId, accepted: value?.accepted === true });
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
return fail(error);
|
|
336
|
+
}
|
|
337
|
+
}));
|
|
338
|
+
server.registerTool('models_list', {
|
|
339
|
+
title: 'List available models',
|
|
340
|
+
description: 'Every model route this harness can currently reach, grouped by provider, with the reasoning efforts each accepts and the deployment default. Call before session_select_model.',
|
|
341
|
+
inputSchema: z.object({}),
|
|
342
|
+
}, tracked('models_list', async () => {
|
|
343
|
+
try {
|
|
344
|
+
const catalog = await controller.modelCatalog();
|
|
345
|
+
return ok({
|
|
346
|
+
default: catalog?.default ?? null,
|
|
347
|
+
providers: (catalog?.groups ?? []).map((group) => ({
|
|
348
|
+
id: group.id,
|
|
349
|
+
name: group.name,
|
|
350
|
+
models: (group.models ?? []).map((model) => ({
|
|
351
|
+
id: model.id,
|
|
352
|
+
name: model.name,
|
|
353
|
+
description: model.description ?? null,
|
|
354
|
+
reasoningEfforts: (model.reasoning?.efforts ?? []).map((effort) => effort.id),
|
|
355
|
+
defaultReasoningEffort: model.reasoning?.defaultEffort ?? null,
|
|
356
|
+
})),
|
|
357
|
+
})),
|
|
358
|
+
unavailable: (catalog?.failures ?? []).map((failure) => ({
|
|
359
|
+
provider: failure.id,
|
|
360
|
+
message: failure.message,
|
|
361
|
+
})),
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
return fail(error);
|
|
366
|
+
}
|
|
367
|
+
}));
|
|
368
|
+
server.registerTool('session_select_model', {
|
|
369
|
+
title: 'Choose the model for a conversation',
|
|
370
|
+
description: 'Pin one provider/model route (and optional reasoning effort) to a conversation. Use models_list first: an unavailable pair is rejected with session/model-unavailable.',
|
|
371
|
+
inputSchema: z.object({
|
|
372
|
+
sessionId: z.string().min(1).describe('Conversation id from sessions_list.'),
|
|
373
|
+
provider: z.string().min(1).describe('Provider id from models_list.'),
|
|
374
|
+
model: z.string().min(1).describe('Model id from models_list.'),
|
|
375
|
+
reasoningEffort: z.string().min(1).optional().describe('Reasoning effort id the route accepts.'),
|
|
376
|
+
}),
|
|
377
|
+
}, tracked('session_select_model', async ({ sessionId, provider, model, reasoningEffort }) => {
|
|
378
|
+
try {
|
|
379
|
+
const request = { sessionId, provider, model };
|
|
380
|
+
if (reasoningEffort !== undefined)
|
|
381
|
+
request.reasoningEffort = reasoningEffort;
|
|
382
|
+
const value = await controller.selectModel(request);
|
|
383
|
+
return ok({ sessionId, selected: value?.selected ?? null });
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
return fail(error);
|
|
387
|
+
}
|
|
388
|
+
}));
|
|
255
389
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -44,6 +44,51 @@ export interface PromptContentPart {
|
|
|
44
44
|
readonly type: 'text';
|
|
45
45
|
readonly text: string;
|
|
46
46
|
}
|
|
47
|
+
/** One reasoning effort a model route accepts. */
|
|
48
|
+
export interface ModelEffort {
|
|
49
|
+
readonly id: string;
|
|
50
|
+
readonly name: string;
|
|
51
|
+
readonly description?: string;
|
|
52
|
+
}
|
|
53
|
+
/** One selectable model inside a provider group. */
|
|
54
|
+
export interface CatalogModel {
|
|
55
|
+
readonly id: string;
|
|
56
|
+
readonly name: string;
|
|
57
|
+
readonly description?: string;
|
|
58
|
+
readonly reasoning?: {
|
|
59
|
+
readonly efforts?: readonly ModelEffort[];
|
|
60
|
+
readonly defaultEffort?: string;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** One provider with its loaded catalog. */
|
|
64
|
+
export interface CatalogProvider {
|
|
65
|
+
readonly id: string;
|
|
66
|
+
readonly name: string;
|
|
67
|
+
readonly models: readonly CatalogModel[];
|
|
68
|
+
}
|
|
69
|
+
/** One provider whose catalog lookup failed. */
|
|
70
|
+
export interface CatalogFailure {
|
|
71
|
+
readonly id: string;
|
|
72
|
+
readonly name: string;
|
|
73
|
+
readonly message: string;
|
|
74
|
+
}
|
|
75
|
+
/** The Host-generation model catalog. */
|
|
76
|
+
export interface ModelCatalog {
|
|
77
|
+
readonly default?: {
|
|
78
|
+
readonly provider: string;
|
|
79
|
+
readonly model: string;
|
|
80
|
+
readonly reasoningEffort?: string;
|
|
81
|
+
};
|
|
82
|
+
readonly routableProviders?: readonly string[];
|
|
83
|
+
readonly groups?: readonly CatalogProvider[];
|
|
84
|
+
readonly failures?: readonly CatalogFailure[];
|
|
85
|
+
}
|
|
86
|
+
/** One durable Session projection read. */
|
|
87
|
+
export interface Projections {
|
|
88
|
+
readonly kind?: string;
|
|
89
|
+
readonly asOfSeq?: number;
|
|
90
|
+
readonly values?: Record<string, unknown>;
|
|
91
|
+
}
|
|
47
92
|
/** The controller surface used by the MCP tools. */
|
|
48
93
|
export interface SessionController {
|
|
49
94
|
list(request: Record<string, never>, signal: AbortSignal): Promise<{
|
|
@@ -65,9 +110,39 @@ export interface SessionController {
|
|
|
65
110
|
}>;
|
|
66
111
|
projections(request: {
|
|
67
112
|
sessionId: string;
|
|
68
|
-
}, signal: AbortSignal): Promise<
|
|
69
|
-
|
|
70
|
-
|
|
113
|
+
}, signal: AbortSignal): Promise<Projections | null>;
|
|
114
|
+
rename(request: {
|
|
115
|
+
sessionId: string;
|
|
116
|
+
title: string;
|
|
117
|
+
}): Promise<{
|
|
118
|
+
title?: string;
|
|
119
|
+
seq?: number;
|
|
120
|
+
}>;
|
|
121
|
+
fork(request: {
|
|
122
|
+
sessionId: string;
|
|
123
|
+
atSeq?: number;
|
|
124
|
+
allowMigration?: boolean;
|
|
125
|
+
}): Promise<{
|
|
126
|
+
sessionId?: string;
|
|
127
|
+
}>;
|
|
128
|
+
cancel(request: {
|
|
129
|
+
sessionId: string;
|
|
130
|
+
}): {
|
|
131
|
+
accepted?: boolean;
|
|
132
|
+
};
|
|
133
|
+
modelCatalog(): Promise<ModelCatalog>;
|
|
134
|
+
selectModel(request: {
|
|
135
|
+
sessionId: string;
|
|
136
|
+
provider: string;
|
|
137
|
+
model: string;
|
|
138
|
+
reasoningEffort?: string;
|
|
139
|
+
}): Promise<{
|
|
140
|
+
selected?: {
|
|
141
|
+
provider?: string;
|
|
142
|
+
model?: string;
|
|
143
|
+
reasoningEffort?: string;
|
|
144
|
+
};
|
|
145
|
+
}>;
|
|
71
146
|
}
|
|
72
147
|
/** Minimal logger surface shared by Cordis and this package's tests. */
|
|
73
148
|
export interface PluginLogger {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nones/dsh-session-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "MCP server for DeepSeek Harness sessions — list, read and prompt live conversations from any MCP client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -57,12 +57,14 @@
|
|
|
57
57
|
"locale/*.json",
|
|
58
58
|
"icon.svg",
|
|
59
59
|
"README.md",
|
|
60
|
+
"CONTRIBUTING.md",
|
|
60
61
|
"LICENSE"
|
|
61
62
|
],
|
|
62
63
|
"scripts": {
|
|
63
64
|
"build": "tsc -p tsconfig.json && tsc -p tsconfig.client.json",
|
|
64
65
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
|
|
65
|
-
"prepublishOnly": "npm run build"
|
|
66
|
+
"prepublishOnly": "npm run build",
|
|
67
|
+
"test": "node test/smoke.mjs"
|
|
66
68
|
},
|
|
67
69
|
"publishConfig": {
|
|
68
70
|
"access": "public",
|
|
@@ -74,6 +76,7 @@
|
|
|
74
76
|
"zod": "^4.6.7"
|
|
75
77
|
},
|
|
76
78
|
"devDependencies": {
|
|
79
|
+
"@modelcontextprotocol/client": "^2.4.0",
|
|
77
80
|
"@types/node": "^26.6.6",
|
|
78
81
|
"typescript": "^7.0.2"
|
|
79
82
|
},
|