@nones/dsh-session-mcp 1.0.2 → 1.1.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/README.md CHANGED
@@ -49,11 +49,18 @@ Restart the harness (or reload the profile) and the endpoint starts with it.
49
49
 
50
50
  ## Get your config
51
51
 
52
- The harness writes everything to:
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:
53
54
 
54
- ```
55
- $DSH_HOME/state/dsh-session-mcp.json
56
- ```
55
+ | Block | For |
56
+ |---|---|
57
+ | JSON — `mcpServers` | Claude Desktop, Cursor, and any JSON-configured client |
58
+ | Codex — `~/.codex/config.toml` | Codex CLI / IDE |
59
+ | Claude Code — CLI | One `claude mcp add …` command |
60
+
61
+ Each block has its own **Copy** button; the endpoint and the token have one too.
62
+
63
+ The same data is on disk at `$DSH_HOME/state/dsh-session-mcp.json`:
57
64
 
58
65
  ```json
59
66
  {
@@ -71,7 +78,7 @@ $DSH_HOME/state/dsh-session-mcp.json
71
78
  }
72
79
  ```
73
80
 
74
- Copy the `client` block into your MCP client. Per client:
81
+ Per client:
75
82
 
76
83
  **Claude Code**
77
84
 
@@ -144,12 +151,20 @@ Written in TypeScript, compiled to `dist/` (committed, so installs need no build
144
151
 
145
152
  | Source | Role |
146
153
  |---|---|
147
- | `src/index.ts` | Cordis plugin: reads/creates the bearer, starts the listener, publishes the state file, closes the listener on unload |
154
+ | `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 |
148
155
  | `src/server.ts` | `node:http` listener with the bearer gate in front of the official SDK's `createMcpHandler` + `toNodeHandler` |
149
156
  | `src/tools.ts` | The five tools, backed directly by `ctx.sessionController` |
150
- | `src/config.ts` | Token persistence, constant-time bearer comparison, client snippet |
157
+ | `src/config.ts` | Token persistence, constant-time bearer comparison, client snippets |
158
+ | `src/client.ts` | The **Settings → MCP** page (classic script for `window.__ModuleLoader__`) |
151
159
  | `src/types.ts` | The narrow structural slice of Harness types this package needs |
152
160
 
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.
167
+
153
168
  ## Development
154
169
 
155
170
  ```bash
File without changes
package/dist/client.js ADDED
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+ /**
3
+ * dsh-session-mcp — Client half.
4
+ *
5
+ * Registers the MCP settings page: the live endpoint, the bearer token, and
6
+ * copy-ready configuration blocks for every client this harness can serve.
7
+ *
8
+ * The page reads the config over the authenticated `/api` channel, so the token
9
+ * never appears in the served HTML.
10
+ *
11
+ * This file is a classic script on purpose: the Web shell loads it through
12
+ * `window.__ModuleLoader__`, and React arrives from the browser module table.
13
+ * Every binding lives inside the factory so re-evaluating the script (HMR)
14
+ * cannot redeclare a global.
15
+ *
16
+ * @module dsh-session-mcp/client
17
+ */
18
+ ;
19
+ globalThis.__ModuleLoader__.load({
20
+ id: '@nones/dsh-session-mcp',
21
+ factory(require) {
22
+ const g = globalThis;
23
+ const React = require('react');
24
+ const h = React.createElement;
25
+ /** Locale namespace owned by this page. */
26
+ const NS = 'dsh-session-mcp';
27
+ /** Where the Host publishes the live config. */
28
+ const CONFIG_URL = '/api/dsh-session-mcp/config';
29
+ const EN = {
30
+ title: 'MCP',
31
+ heading: 'MCP endpoint',
32
+ description: 'Connect Claude Code, Codex, Cursor or any other MCP client to this harness. The endpoint starts together with the harness and requires the bearer token below.',
33
+ endpoint: 'Endpoint',
34
+ bearer: 'Bearer token',
35
+ show: 'Show',
36
+ hide: 'Hide',
37
+ copy: 'Copy',
38
+ copied: 'Copied',
39
+ jsonTitle: 'JSON — mcpServers',
40
+ codexTitle: 'Codex — ~/.codex/config.toml',
41
+ claudeTitle: 'Claude Code — CLI',
42
+ note: 'Paste any block into your client. The token is stable across restarts; it rotates only when you delete $DSH_HOME/state/dsh-session-mcp.json and restart the harness.',
43
+ loading: 'Loading…',
44
+ error: 'Could not read the endpoint config: ',
45
+ };
46
+ const RU = {
47
+ title: 'MCP',
48
+ heading: 'MCP-эндпоинт',
49
+ description: 'Подключите Claude Code, Codex, Cursor или любой другой MCP-клиент к этому харнесу. Эндпоинт поднимается вместе с харнесом и требует Bearer-токен ниже.',
50
+ endpoint: 'Эндпоинт',
51
+ bearer: 'Bearer-токен',
52
+ show: 'Показать',
53
+ hide: 'Скрыть',
54
+ copy: 'Копировать',
55
+ copied: 'Скопировано',
56
+ jsonTitle: 'JSON — mcpServers',
57
+ codexTitle: 'Codex — ~/.codex/config.toml',
58
+ claudeTitle: 'Claude Code — CLI',
59
+ note: 'Вставьте любой блок в свой клиент. Токен не меняется между перезапусками; он обновится только если удалить $DSH_HOME/state/dsh-session-mcp.json и перезапустить харнес.',
60
+ loading: 'Загрузка…',
61
+ error: 'Не удалось прочитать конфиг эндпоинта: ',
62
+ };
63
+ /** Theme-bound inline styles; no colors are hardcoded. */
64
+ const styles = {
65
+ page: {
66
+ display: 'grid',
67
+ gap: '16px',
68
+ maxWidth: '760px',
69
+ fontSize: '13px',
70
+ lineHeight: 1.5,
71
+ color: 'var(--dsw-alias-label-primary)',
72
+ },
73
+ heading: { margin: 0, fontSize: '16px', fontWeight: 600 },
74
+ description: { margin: 0, color: 'var(--dsw-alias-label-secondary)' },
75
+ card: {
76
+ display: 'grid',
77
+ gap: '10px',
78
+ padding: '14px 16px',
79
+ border: '1px solid var(--dsw-alias-border-l1)',
80
+ borderRadius: '12px',
81
+ background: 'var(--dsw-alias-bg-layer-1)',
82
+ },
83
+ cardHead: {
84
+ display: 'flex',
85
+ alignItems: 'center',
86
+ justifyContent: 'space-between',
87
+ gap: '12px',
88
+ },
89
+ cardTitle: { fontSize: '12px', fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' },
90
+ row: {
91
+ display: 'flex',
92
+ alignItems: 'center',
93
+ justifyContent: 'space-between',
94
+ gap: '12px',
95
+ },
96
+ rowLabel: { color: 'var(--dsw-alias-label-secondary)', whiteSpace: 'nowrap' },
97
+ rowValue: {
98
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
99
+ fontSize: '12px',
100
+ wordBreak: 'break-all',
101
+ flex: '1 1 auto',
102
+ textAlign: 'right',
103
+ },
104
+ actions: { display: 'flex', gap: '6px', flex: '0 0 auto' },
105
+ button: {
106
+ font: 'inherit',
107
+ fontSize: '12px',
108
+ padding: '3px 10px',
109
+ borderRadius: '8px',
110
+ border: '1px solid var(--dsw-alias-border-l2)',
111
+ background: 'var(--dsw-alias-bg-layer-2)',
112
+ color: 'var(--dsw-alias-label-primary)',
113
+ cursor: 'pointer',
114
+ },
115
+ pre: {
116
+ margin: 0,
117
+ padding: '12px',
118
+ borderRadius: '8px',
119
+ background: 'var(--dsw-alias-bg-layer-2)',
120
+ border: '1px solid var(--dsw-alias-border-l1)',
121
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
122
+ fontSize: '12px',
123
+ lineHeight: 1.45,
124
+ overflow: 'auto',
125
+ maxHeight: '220px',
126
+ whiteSpace: 'pre',
127
+ },
128
+ note: { margin: 0, color: 'var(--dsw-alias-label-secondary)', fontSize: '12px' },
129
+ error: { margin: 0, color: 'var(--dsw-alias-state-error-primary)' },
130
+ };
131
+ return {
132
+ inject: ['slots', 'locale'],
133
+ apply(ctx) {
134
+ ctx.effect(() => ctx.locale.register(NS, 'en', EN));
135
+ ctx.effect(() => ctx.locale.register(NS, 'ru', RU));
136
+ const t = ctx.locale.bind(NS);
137
+ /** Re-render on locale switches so the page follows the active language. */
138
+ function useLocaleRevision() {
139
+ const [, bump] = React.useState(0);
140
+ React.useEffect(() => ctx.locale.subscribe(() => bump((n) => n + 1)), []);
141
+ }
142
+ function CopyButton(props) {
143
+ const [copied, setCopied] = React.useState(false);
144
+ const onCopy = React.useCallback(() => {
145
+ const clipboard = g.navigator?.clipboard;
146
+ if (clipboard === undefined)
147
+ return;
148
+ clipboard.writeText(props.text).then(() => {
149
+ setCopied(true);
150
+ g.setTimeout(() => setCopied(false), 1600);
151
+ }, () => undefined);
152
+ }, [props.text]);
153
+ return h('button', { type: 'button', style: styles.button, onClick: onCopy }, copied ? t('copied') : t('copy'));
154
+ }
155
+ function Snippet(props) {
156
+ return h('section', { style: styles.card }, h('div', { style: styles.cardHead }, h('span', { style: styles.cardTitle }, props.title), h(CopyButton, { text: props.text })), h('pre', { style: styles.pre }, props.text));
157
+ }
158
+ function Section() {
159
+ useLocaleRevision();
160
+ const [state, setState] = React.useState({ status: 'loading' });
161
+ const [revealed, setRevealed] = React.useState(false);
162
+ React.useEffect(() => {
163
+ let alive = true;
164
+ g.fetch(CONFIG_URL, { headers: { accept: 'application/json' }, credentials: 'same-origin' })
165
+ .then((response) => response.ok ? response.json() : Promise.reject(new Error(`HTTP ${String(response.status)}`)))
166
+ .then((data) => {
167
+ if (alive)
168
+ setState({ status: 'ready', data });
169
+ })
170
+ .catch((error) => {
171
+ if (alive)
172
+ setState({ status: 'error', message: String(error?.message ?? error) });
173
+ });
174
+ return () => {
175
+ alive = false;
176
+ };
177
+ }, []);
178
+ const header = h('div', { style: { display: 'grid', gap: '4px' } }, h('h2', { style: styles.heading }, t('heading')), h('p', { style: styles.description }, t('description')));
179
+ if (state.status === 'loading') {
180
+ return h('div', { style: styles.page }, header, h('p', { style: styles.note }, t('loading')));
181
+ }
182
+ if (state.status === 'error') {
183
+ return h('div', { style: styles.page }, header, h('p', { style: styles.error }, `${t('error')}${String(state.message)}`));
184
+ }
185
+ const data = state.data;
186
+ const token = String(data.token);
187
+ const masked = `${token.slice(0, 6)}…${token.slice(-4)}`;
188
+ return h('div', { style: styles.page }, header, h('section', { style: styles.card }, h('div', { style: styles.row }, h('span', { style: styles.rowLabel }, t('endpoint')), h('span', { style: styles.rowValue }, data.url), h('div', { style: styles.actions }, h(CopyButton, { text: data.url }))), h('div', { style: styles.row }, h('span', { style: styles.rowLabel }, t('bearer')), h('span', { style: styles.rowValue }, revealed ? token : masked), h('div', { style: styles.actions }, h('button', {
189
+ type: 'button',
190
+ style: styles.button,
191
+ onClick: () => setRevealed((value) => !value),
192
+ }, revealed ? t('hide') : t('show')), h(CopyButton, { text: token })))), h(Snippet, { title: t('jsonTitle'), text: data.snippets.json }), h(Snippet, { title: t('codexTitle'), text: data.snippets.codexToml }), h(Snippet, { title: t('claudeTitle'), text: data.snippets.claudeCode }), h('p', { style: styles.note }, t('note')));
193
+ }
194
+ ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: NS, order: 30, label: () => t('title') }, Section));
195
+ },
196
+ };
197
+ },
198
+ });
package/dist/config.d.ts CHANGED
@@ -75,3 +75,31 @@ export declare function bearerMatches(expected: string, header: string | undefin
75
75
  * @returns an `mcpServers` fragment accepted by most MCP clients.
76
76
  */
77
77
  export declare function clientConfig(url: string, token: string): ClientConfig;
78
+ /** Path of the authenticated config route the Settings page reads. */
79
+ export declare const CONFIG_ROUTE_PATH = "/api/dsh-session-mcp/config";
80
+ /** Every snippet the Settings page offers for copying. */
81
+ export interface ConfigSnippets {
82
+ /** `mcpServers` JSON for Claude Desktop, Cursor and other JSON clients. */
83
+ readonly json: string;
84
+ /** `~/.codex/config.toml` block. */
85
+ readonly codexToml: string;
86
+ /** One `claude mcp add` command. */
87
+ readonly claudeCode: string;
88
+ }
89
+ /**
90
+ * Render the copy-paste snippets for one endpoint.
91
+ * @param url - full MCP endpoint URL.
92
+ * @param token - bearer token.
93
+ * @returns the three snippets the Settings page shows.
94
+ */
95
+ export declare function snippetsFor(url: string, token: string): ConfigSnippets;
96
+ /** What the Settings page receives from the Host. */
97
+ export interface ConfigPayload {
98
+ readonly name: string;
99
+ readonly url: string;
100
+ readonly host: string;
101
+ readonly port: number;
102
+ readonly token: string;
103
+ readonly updatedAt: string;
104
+ readonly snippets: ConfigSnippets;
105
+ }
package/dist/config.js CHANGED
@@ -120,3 +120,24 @@ export function clientConfig(url, token) {
120
120
  },
121
121
  };
122
122
  }
123
+ /** Path of the authenticated config route the Settings page reads. */
124
+ export const CONFIG_ROUTE_PATH = '/api/dsh-session-mcp/config';
125
+ /**
126
+ * Render the copy-paste snippets for one endpoint.
127
+ * @param url - full MCP endpoint URL.
128
+ * @param token - bearer token.
129
+ * @returns the three snippets the Settings page shows.
130
+ */
131
+ export function snippetsFor(url, token) {
132
+ return {
133
+ json: JSON.stringify(clientConfig(url, token), null, 2),
134
+ codexToml: [
135
+ `[mcp_servers.${CLIENT_KEY}]`,
136
+ `url = "${url}"`,
137
+ '',
138
+ `[mcp_servers.${CLIENT_KEY}.http_headers]`,
139
+ `Authorization = "Bearer ${token}"`,
140
+ ].join('\n'),
141
+ claudeCode: `claude mcp add --transport http ${CLIENT_KEY} ${url} --header "Authorization: Bearer ${token}"`,
142
+ };
143
+ }
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * @module dsh-session-mcp
14
14
  */
15
15
  import { readFileSync } from 'node:fs';
16
- import { clientConfig, createToken, DEFAULT_HOST, DEFAULT_PORT, readState, SERVER_NAME, writeState, } from './config.js';
16
+ import { clientConfig, CONFIG_ROUTE_PATH, createToken, DEFAULT_HOST, DEFAULT_PORT, readState, SERVER_NAME, snippetsFor, writeState, } from './config.js';
17
17
  import { startEndpoint } from './server.js';
18
18
  /** Stable Cordis plugin name. */
19
19
  export const name = 'dsh-session-mcp';
@@ -85,6 +85,31 @@ export async function apply(ctx, config) {
85
85
  ctx.logger?.info?.(`dsh-session-mcp: bearer token and client config in ${file}`);
86
86
  console.log(`dsh-session-mcp: ${endpoint.url} (bearer in ${file})`);
87
87
  console.log(JSON.stringify(state.client, null, 2));
88
+ // The Settings page reads the live config over the authenticated `/api`
89
+ // channel, so the token never enters the served HTML.
90
+ const payload = {
91
+ name: SERVER_NAME,
92
+ url: endpoint.url,
93
+ host,
94
+ port: endpoint.port,
95
+ token,
96
+ updatedAt: state.updatedAt,
97
+ snippets: snippetsFor(endpoint.url, token),
98
+ };
99
+ ctx.inject(['connection'], (connectionCtx) => {
100
+ connectionCtx.connection.fetch.register({
101
+ path: CONFIG_ROUTE_PATH,
102
+ methods: ['GET'],
103
+ requestBody: 'buffered',
104
+ fetch: async () => new Response(JSON.stringify(payload), {
105
+ status: 200,
106
+ headers: {
107
+ 'content-type': 'application/json; charset=utf-8',
108
+ 'cache-control': 'no-store',
109
+ },
110
+ }),
111
+ });
112
+ });
88
113
  ctx.effect(() => () => {
89
114
  void endpoint.close();
90
115
  }, 'dsh-session-mcp: endpoint lifetime');
package/dist/types.d.ts CHANGED
@@ -79,4 +79,20 @@ export interface PluginContext {
79
79
  readonly sessionController: SessionController;
80
80
  readonly logger?: PluginLogger;
81
81
  effect(setup: () => () => void, label?: string): void;
82
+ inject(deps: readonly string[], callback: (ctx: ConnectionContext) => void): void;
83
+ }
84
+ /** One exact Fetch route on the authenticated `/api` channel. */
85
+ export interface FetchRoute {
86
+ readonly path: string;
87
+ readonly methods: readonly string[];
88
+ readonly requestBody: 'buffered' | 'streaming';
89
+ readonly fetch: (request: Request) => Promise<Response>;
90
+ }
91
+ /** The slice of the Client Connection service a Host plugin can use. */
92
+ export interface ConnectionContext {
93
+ readonly connection: {
94
+ readonly fetch: {
95
+ register(route: FetchRoute): unknown;
96
+ };
97
+ };
82
98
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nones/dsh-session-mcp",
3
- "version": "1.0.2",
3
+ "version": "1.1.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",
@@ -45,7 +45,11 @@
45
45
  "default": "./dist/config.js"
46
46
  },
47
47
  "./locale/*.json": "./locale/*.json",
48
- "./icon": "./icon.svg"
48
+ "./icon": "./icon.svg",
49
+ "./client": {
50
+ "types": "./dist/client.d.ts",
51
+ "default": "./dist/client.js"
52
+ }
49
53
  },
50
54
  "files": [
51
55
  "dist",
@@ -56,8 +60,8 @@
56
60
  "LICENSE"
57
61
  ],
58
62
  "scripts": {
59
- "build": "tsc -p tsconfig.json",
60
- "typecheck": "tsc -p tsconfig.json --noEmit",
63
+ "build": "tsc -p tsconfig.json && tsc -p tsconfig.client.json",
64
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
61
65
  "prepublishOnly": "npm run build"
62
66
  },
63
67
  "publishConfig": {
@@ -76,6 +80,13 @@
76
80
  "dsh": {
77
81
  "bundle": {
78
82
  "patch": "./cordis.patch.yml"
83
+ },
84
+ "client": {
85
+ "platform": "web",
86
+ "immediately": true,
87
+ "inject": [
88
+ "@deepseek-ai/dsh-client-ui-settings"
89
+ ]
79
90
  }
80
91
  }
81
92
  }