@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.
@@ -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 list, read and prompt the
5
- conversations running in your harness.
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** and writes a ready-to-paste client config. No
9
- sidecar process to babysit, no port to guess, and no second writer on the session log.
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
- ## Get your config
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
- Open **Settings → MCP** in the harness. The page shows the live endpoint, the bearer
53
- token (masked until you reveal it), and three copy-ready blocks:
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
- Per client:
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
- - **Rotate** by deleting that file and restarting the harness. Every client then needs the
135
- new token.
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 five tools, backed directly by `ctx.sessionController` |
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
- The Settings page reads `/api/dsh-session-mcp/config` over the authenticated browser
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
- ## Development
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
- bun install
172
- bun run typecheck # tsc --noEmit
173
- bun run build # tsc -> dist/
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
- Requires Node ≥ 20. Runtime dependencies: `@modelcontextprotocol/server`,
177
- `@modelcontextprotocol/node`, `zod`. Dev: `typescript`, `@types/node`.
228
+ The workflow requires one repository secret, `NPM_TOKEN` (npm automation token).
178
229
 
179
- `dist/` is committed so `bun add github:ZeiZel/dsh-session-mcp` and
180
- `dsh plugin add <dir>` work without a build step; `prepublishOnly` rebuilds it for npm.
230
+ ## Contributing
181
231
 
182
- Releases: push a `v*` tag and the `release` workflow builds, publishes to npm and cuts a
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
- asOfSeq?: number;
70
- } | null>;
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.2.0",
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
  },