@hzxwonder/dsh-mcp 0.3.0 → 0.5.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/.agents/skills/deepseek-worker/SKILL.md +70 -10
- package/README.md +162 -157
- package/config/routing.md +59 -4
- package/config/worker.patch.yml +13 -2
- package/package.json +9 -1
- package/server.json +21 -6
- package/src/cli.js +4 -0
- package/src/doctor.js +230 -0
- package/src/provider.js +301 -5
- package/src/server.js +12 -6
- package/src/session-output.js +72 -0
- package/src/setup-terminal.js +183 -0
- package/src/setup.js +110 -81
- package/src/worker.js +65 -11
- package/src/setup-ui.js +0 -112
package/config/routing.md
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
## DeepSeek task delegation
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
Keep the current GPT model, provider, and account.
|
|
4
|
+
Every delegate_to_deepseek call needs the current authorized absolute project_root and an explicit thinking_effort from the selected model's current tool schema.
|
|
5
|
+
Include both on resume. Never infer the root from the server or installation. Never default effort.
|
|
6
|
+
Use read-only unless the task needs edits. Read the session answer, reason, and diagnostics before review. Follow user and project restrictions.
|
|
7
|
+
The full Harness runs locally with the protocol, API service, and model selected in setup.
|
|
8
|
+
The configured API service receives task data and charges its own API fees.
|
|
9
|
+
Never silently replace the selected model with DeepSeek Flash or a direct API call.
|
|
7
10
|
|
|
8
11
|
- Use the installed `deepseek-worker` skill for eligible tasks.
|
|
9
12
|
- Delegate worthwhile, bounded, low-risk tasks by default when the MCP tool is available.
|
|
@@ -13,6 +16,15 @@ Never infer this path from the server or installation. Use read-only unless the
|
|
|
13
16
|
- Use the active project root unless the user explicitly authorizes a different project directory.
|
|
14
17
|
- Do not infer the root from the server directory, home directory, installation, or previous calls.
|
|
15
18
|
- Do not delegate when the current project root is unknown.
|
|
19
|
+
- Select `thinking_effort` consciously for every call, including continuation.
|
|
20
|
+
- The tool schema exposes a model-dependent subset of `off`, `low`, `high`, and `max`.
|
|
21
|
+
- Unknown model metadata exposes only `off`. This limit is not proof that the model cannot reason.
|
|
22
|
+
- Use `low` for mechanical tasks and `high` for bounded reasoning only when the schema permits them.
|
|
23
|
+
- Use `off` deliberately, including when the schema permits only `off`.
|
|
24
|
+
- Use `max` only when the schema permits it and the task justifies its additional cost.
|
|
25
|
+
- Never silently supply missing effort or use aliases such as `medium` or `xhigh`.
|
|
26
|
+
- Never coerce an unsupported effort to another value.
|
|
27
|
+
- This parameter sets DSH `agent-default-model.config.reasoningEffort`, not prompt text.
|
|
16
28
|
- Each `project_root` grants directory access for that call only.
|
|
17
29
|
- The server does not independently check this grant against a Codex workspace ACL.
|
|
18
30
|
- This grant is not a read isolation boundary.
|
|
@@ -27,8 +39,51 @@ Never infer this path from the server or installation. Use read-only unless the
|
|
|
27
39
|
- Require the worker to stop and report unresolved questions in its final answer.
|
|
28
40
|
- Do not promise live dialogue or automatic approval responses during a call.
|
|
29
41
|
- Resolve questions before a new call with the returned `session_id`, the same `project_root`, and the same `cwd`.
|
|
42
|
+
- Select `thinking_effort` again when you resume. Do not depend on the old session setting.
|
|
43
|
+
- After the worker stops, MCP reads the persisted session and checks its ID and saved physical working directory.
|
|
44
|
+
- Read `answer`, `output_source: "session"`, `session_id`, `session_file`, `assistant_seq`, and `turn`.
|
|
45
|
+
- Read `reason`, `terminal_reason`, `session_output_error`, and `diagnostics` before reviewing actual changes.
|
|
46
|
+
- The answer is the last committed assistant text in the latest durable turn.
|
|
47
|
+
- Hidden reasoning, tool calls, and tool results are not final answers.
|
|
48
|
+
- An empty latest turn returns empty text and a null assistant sequence, never an earlier successful answer.
|
|
49
|
+
- Session read failures are explicit. Do not substitute stdout or old answers.
|
|
50
|
+
- The persisted turn must match this invocation's turn_start event. Never return a previous turn after a failed resume.
|
|
51
|
+
- Do not promise a separate arbitrary-session reader.
|
|
30
52
|
- Review actual changes and check evidence before acceptance.
|
|
31
53
|
- Treat `completed` as Harness turn completion, not task acceptance.
|
|
32
54
|
- Report unverified results and partial changes explicitly.
|
|
33
55
|
|
|
34
56
|
These instructions guide delegation decisions but cannot guarantee automatic tool invocation.
|
|
57
|
+
|
|
58
|
+
Example arguments:
|
|
59
|
+
|
|
60
|
+
This example assumes that the current model's schema permits `low`.
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"project_root": "/absolute/authorized/project",
|
|
65
|
+
"thinking_effort": "low",
|
|
66
|
+
"task": "Correct the specified documentation typo. Preserve user changes. Do not commit or delegate. Report the changed file and any checks.",
|
|
67
|
+
"cwd": ".",
|
|
68
|
+
"permission_mode": "workspace-write"
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Global setup needs no project configuration.
|
|
73
|
+
The installer can upgrade an owned, unchanged previous skill template.
|
|
74
|
+
It preserves customized skills and skills without a previous-template reference.
|
|
75
|
+
Back up those files and resolve conflicts manually before replacement.
|
|
76
|
+
|
|
77
|
+
Setup runs in the terminal and configures protocol, Base URL, hidden API key, and a user-selected remote model.
|
|
78
|
+
Rerun the same `dsh-mcp setup` command to edit saved settings or rotate the key from the masked summary.
|
|
79
|
+
An unavailable model list permits an explicit manual model ID, not a fake discovery result.
|
|
80
|
+
The real tiny inference test can incur an API charge.
|
|
81
|
+
Only a successful inference test produces `✅ Connection successful!`.
|
|
82
|
+
That message does not prove MCP registration or Codex App availability.
|
|
83
|
+
Use `dsh-mcp doctor` to inspect effective configuration, stdio initialization, and the tool catalog without a paid inference request.
|
|
84
|
+
A healthy independent handshake does not prove that the current App session exposes the tool.
|
|
85
|
+
Use the same host, user, and `CODEX_HOME` as Codex.
|
|
86
|
+
If necessary, ask the user to save work and fully restart Codex before starting a new session.
|
|
87
|
+
Do not stop the user's client or inspect private login files.
|
|
88
|
+
Unsupported OAuth for stdio is not a provider API key failure.
|
|
89
|
+
Never include credentials in project configuration, task text, commands, logs, or screenshots.
|
package/config/worker.patch.yml
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
- id: agent-default-model
|
|
2
2
|
config:
|
|
3
|
-
provider:
|
|
4
|
-
model:
|
|
3
|
+
provider: !!js process.env.DSH_MCP_PROVIDER
|
|
4
|
+
model: !!js process.env.DSH_MCP_MODEL
|
|
5
|
+
reasoningEffort: !!js process.env.DSH_MCP_THINKING_EFFORT
|
|
6
|
+
- id: llm-pi-ai
|
|
7
|
+
config:
|
|
8
|
+
providers: !!js JSON.parse(process.env.DSH_MCP_PI_PROVIDERS)
|
|
9
|
+
- id: llm-deepseek
|
|
10
|
+
disabled: !!js process.env.DSH_MCP_PROVIDER !== 'deepseek-official'
|
|
11
|
+
config: !!js JSON.parse(process.env.DSH_MCP_DEEPSEEK_PROFILE)
|
|
12
|
+
- id: llm-deepseek-account
|
|
13
|
+
disabled: true
|
|
14
|
+
- id: compaction-basic
|
|
15
|
+
config: !!js JSON.parse(process.env.DSH_MCP_COMPACTION_PROFILE)
|
|
5
16
|
- id: settings
|
|
6
17
|
disabled: true
|
|
7
18
|
- id: config-editor
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hzxwonder/dsh-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"mcpName": "io.github.hzxwonder/dsh-mcp",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Delegate bounded Codex tasks to DeepSeek Harness over MCP stdio.",
|
|
@@ -35,7 +35,15 @@
|
|
|
35
35
|
"dsh": "dsh"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
+
"@deepseek-ai/cordis": "4.1.0",
|
|
38
39
|
"@deepseek-ai/dsh": "0.2.2-rc.1",
|
|
40
|
+
"@deepseek-ai/dsh-llm": "0.2.2-rc.1",
|
|
41
|
+
"@deepseek-ai/dsh-llm-deepseek": "0.2.2-rc.1",
|
|
42
|
+
"@deepseek-ai/dsh-llm-pi-ai": "0.2.2-rc.1",
|
|
43
|
+
"@deepseek-ai/dsh-session": "0.2.2-rc.1",
|
|
44
|
+
"@deepseek-ai/dsh-session-format-catalog": "0.2.2-rc.1",
|
|
45
|
+
"@deepseek-ai/dsh-session-persistence-jsonl": "0.2.2-rc.1",
|
|
46
|
+
"@earendil-works/pi-ai": "1.5.0",
|
|
39
47
|
"@modelcontextprotocol/sdk": "1.32.2",
|
|
40
48
|
"smol-toml": "1.9.1",
|
|
41
49
|
"zod": "^4.0.0"
|
package/server.json
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.hzxwonder/dsh-mcp",
|
|
4
4
|
"title": "DeepSeek Harness for Codex",
|
|
5
|
-
"description": "
|
|
6
|
-
"version": "0.
|
|
5
|
+
"description": "Run local Harness tasks through Chat Completions, Responses, or Anthropic Messages.",
|
|
6
|
+
"version": "0.5.0",
|
|
7
7
|
"repository": {
|
|
8
8
|
"url": "https://github.com/hzxwonder/dsh-mcp",
|
|
9
9
|
"source": "github"
|
|
@@ -14,26 +14,41 @@
|
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
16
16
|
"identifier": "@hzxwonder/dsh-mcp",
|
|
17
|
-
"version": "0.
|
|
17
|
+
"version": "0.5.0",
|
|
18
18
|
"runtimeHint": "npx",
|
|
19
19
|
"transport": {
|
|
20
20
|
"type": "stdio"
|
|
21
21
|
},
|
|
22
22
|
"environmentVariables": [
|
|
23
|
+
{
|
|
24
|
+
"name": "DSH_MCP_PROTOCOL",
|
|
25
|
+
"description": "Wire protocol. The terminal setup command discovers models and validates the selected protocol.",
|
|
26
|
+
"format": "string",
|
|
27
|
+
"choices": ["chat-completions", "responses", "anthropic"],
|
|
28
|
+
"default": "chat-completions",
|
|
29
|
+
"isRequired": false,
|
|
30
|
+
"isSecret": false
|
|
31
|
+
},
|
|
23
32
|
{
|
|
24
33
|
"name": "DEEPSEEK_API_KEY",
|
|
25
|
-
"description": "API Key for
|
|
34
|
+
"description": "API Key for the selected provider. Pass this secret through the environment, never command arguments.",
|
|
26
35
|
"format": "string",
|
|
27
36
|
"isRequired": true,
|
|
28
37
|
"isSecret": true
|
|
29
38
|
},
|
|
30
39
|
{
|
|
31
40
|
"name": "DEEPSEEK_BASE_URL",
|
|
32
|
-
"description": "
|
|
41
|
+
"description": "API root. Leave empty for the selected protocol's default. Use terminal setup for model discovery.",
|
|
33
42
|
"format": "string",
|
|
34
|
-
"default": "https://api.deepseek.com/anthropic",
|
|
35
43
|
"isRequired": false,
|
|
36
44
|
"isSecret": false
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "DSH_MCP_MODEL",
|
|
48
|
+
"description": "Model ID available to this API key. Terminal setup can discover the model list.",
|
|
49
|
+
"format": "string",
|
|
50
|
+
"isRequired": true,
|
|
51
|
+
"isSecret": false
|
|
37
52
|
}
|
|
38
53
|
]
|
|
39
54
|
}
|
package/src/cli.js
CHANGED
|
@@ -5,6 +5,10 @@ try {
|
|
|
5
5
|
process.argv.splice(2, 1);
|
|
6
6
|
const { runSetup } = await import('./setup.js');
|
|
7
7
|
await runSetup();
|
|
8
|
+
} else if (process.argv[2] === 'doctor') {
|
|
9
|
+
process.argv.splice(2, 1);
|
|
10
|
+
const { runDoctor } = await import('./doctor.js');
|
|
11
|
+
await runDoctor();
|
|
8
12
|
} else {
|
|
9
13
|
await import('./server.js');
|
|
10
14
|
}
|
package/src/doctor.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { access, readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { basename, delimiter, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
6
|
+
import { isDeepStrictEqual, parseArgs, promisify } from 'node:util';
|
|
7
|
+
import { parse } from 'smol-toml';
|
|
8
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
9
|
+
import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
10
|
+
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
const requiredTool = 'delegate_to_deepseek';
|
|
13
|
+
const reloadHint = 'This checks a new MCP connection, not the running Codex Desktop connection. If dsh is absent there, fully quit and reopen Codex, then start a new chat and inspect its MCP status. No client was restarted.';
|
|
14
|
+
const authHint = 'Auth unsupported is normal for a local stdio MCP server; it does not report the worker API key status. No model/API request was made.';
|
|
15
|
+
const rootHint = 'Each delegation still needs an absolute project_root and explicit thinking_effort; registration does not choose a workspace.';
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async function executablePath(command, env, cwd = process.cwd()) {
|
|
19
|
+
const candidates = isAbsolute(command) || command.includes('/') || command.includes('\\')
|
|
20
|
+
? [resolve(cwd, command)]
|
|
21
|
+
: (env.PATH || '').split(delimiter).filter(Boolean).map((directory) => resolve(cwd, directory, command));
|
|
22
|
+
for (const candidate of candidates) {
|
|
23
|
+
try {
|
|
24
|
+
await access(candidate, constants.X_OK);
|
|
25
|
+
if ((await stat(candidate)).isFile()) return candidate;
|
|
26
|
+
} catch { /* Try the next PATH entry. */ }
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function pathHint(path) {
|
|
32
|
+
return /(?:^|[/\\])(?:_npx|node_modules)(?:[/\\]|$)/.test(path)
|
|
33
|
+
? 'The registered package/cache path is stale or incomplete. Rerun dsh-mcp setup from the current installed version to replace the old cached path.'
|
|
34
|
+
: 'Rerun dsh-mcp setup from an existing installation to refresh the absolute executable and server script paths.';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Report only diagnostic categories, never arbitrary child output, config values, or prompts.
|
|
38
|
+
function startupDetail(error, stderr = '') {
|
|
39
|
+
const text = `${error?.code || ''} ${error?.message || ''}\n${stderr}`;
|
|
40
|
+
const codes = [...new Set(text.match(/\b(?:ERR_MODULE_NOT_FOUND|ERR_REQUIRE_ESM|ERR_PACKAGE_PATH_NOT_EXPORTED|ERR_DLOPEN_FAILED|ERR_UNKNOWN_FILE_EXTENSION|ERR_INVALID_ARG_TYPE|ERR_ACCESS_DENIED|ERR_INVALID_PACKAGE_CONFIG|MODULE_NOT_FOUND|ENOENT|EACCES|ENOTDIR|SyntaxError|ReferenceError|TypeError)\b/g) || [])];
|
|
41
|
+
if (codes.length) return codes.slice(0, 5).join(', ');
|
|
42
|
+
if (/timed?\s*out|timeout|abort/i.test(text)) return 'startup/discovery timed out';
|
|
43
|
+
if (/connection closed|EPIPE/i.test(text)) return 'connection closed during startup/discovery';
|
|
44
|
+
if (/JSON|Unexpected token/i.test(text)) return 'invalid MCP JSON output';
|
|
45
|
+
return 'server startup or MCP protocol error';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function inspectCodexCli(entry, configPath, timeoutMs) {
|
|
49
|
+
const command = await executablePath('codex', getDefaultEnvironment());
|
|
50
|
+
if (!command) return { available: false, ok: null, enabled: null, error: null };
|
|
51
|
+
try {
|
|
52
|
+
const { stdout } = await execFileAsync(command, ['mcp', 'get', 'dsh', '--json'], {
|
|
53
|
+
env: { ...process.env, CODEX_HOME: dirname(configPath) },
|
|
54
|
+
timeout: timeoutMs, maxBuffer: 1024 * 1024, windowsHide: true,
|
|
55
|
+
});
|
|
56
|
+
const effective = JSON.parse(stdout);
|
|
57
|
+
const transport = effective.transport;
|
|
58
|
+
const matches = effective.enabled !== false && transport?.type === 'stdio'
|
|
59
|
+
&& transport.command === entry.command
|
|
60
|
+
&& JSON.stringify(transport.args || []) === JSON.stringify(entry.args || [])
|
|
61
|
+
&& (transport.cwd || null) === (entry.cwd || null)
|
|
62
|
+
&& isDeepStrictEqual(transport.env || {}, entry.env || {})
|
|
63
|
+
&& isDeepStrictEqual(transport.env_vars || [], entry.env_vars || [])
|
|
64
|
+
&& !effective.disabled_tools?.includes(requiredTool)
|
|
65
|
+
&& (!effective.enabled_tools || effective.enabled_tools.includes(requiredTool));
|
|
66
|
+
return {
|
|
67
|
+
available: true, ok: matches, enabled: effective.enabled !== false,
|
|
68
|
+
error: matches ? null : 'The Codex CLI effective dsh entry is disabled or differs from this stdio registration.',
|
|
69
|
+
};
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return { available: true, ok: false, enabled: null, error: `Codex CLI could not read the effective dsh registration (${startupDetail(error)}).` };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function inspectRegistration({
|
|
76
|
+
configPath = join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'config.toml'),
|
|
77
|
+
timeoutMs = 12000,
|
|
78
|
+
checkCodexCli = true,
|
|
79
|
+
} = {}) {
|
|
80
|
+
const result = {
|
|
81
|
+
ok: false, configPath: resolve(configPath), serverVersion: null, tools: [],
|
|
82
|
+
codexCli: null, error: null, hints: [authHint, rootHint],
|
|
83
|
+
};
|
|
84
|
+
let client;
|
|
85
|
+
let transport;
|
|
86
|
+
let stderr = '';
|
|
87
|
+
try {
|
|
88
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error('Provide a positive timeoutMs.');
|
|
89
|
+
let config;
|
|
90
|
+
try {
|
|
91
|
+
config = parse(await readFile(result.configPath, 'utf8'));
|
|
92
|
+
} catch (error) {
|
|
93
|
+
throw new Error(error.code === 'ENOENT'
|
|
94
|
+
? 'Codex config.toml does not exist. Run dsh-mcp setup for this CODEX_HOME.'
|
|
95
|
+
: 'Codex config.toml cannot be read or parsed. Check its permissions and TOML syntax.');
|
|
96
|
+
}
|
|
97
|
+
const entry = config.mcp_servers?.dsh;
|
|
98
|
+
if (!entry || typeof entry !== 'object') throw new Error('No mcp_servers.dsh entry exists. Run dsh-mcp setup for this CODEX_HOME.');
|
|
99
|
+
if (entry.enabled === false) throw new Error('The dsh MCP entry is disabled. Enable mcp_servers.dsh in the selected Codex config.');
|
|
100
|
+
if ((entry.disabled_tools !== undefined && (!Array.isArray(entry.disabled_tools) || entry.disabled_tools.some((name) => typeof name !== 'string')))
|
|
101
|
+
|| (entry.enabled_tools !== undefined && (!Array.isArray(entry.enabled_tools) || entry.enabled_tools.some((name) => typeof name !== 'string')))) {
|
|
102
|
+
throw new Error('The dsh MCP enabled_tools and disabled_tools must be arrays of tool names.');
|
|
103
|
+
}
|
|
104
|
+
if (entry.disabled_tools?.includes(requiredTool) || (entry.enabled_tools && !entry.enabled_tools.includes(requiredTool))) {
|
|
105
|
+
throw new Error('Codex tool filters hide delegate_to_deepseek. Remove it from disabled_tools and include it in enabled_tools, if configured.');
|
|
106
|
+
}
|
|
107
|
+
if (typeof entry.command !== 'string' || !entry.command.trim() || entry.url) {
|
|
108
|
+
throw new Error('The dsh MCP entry must use a local stdio command. Rerun dsh-mcp setup.');
|
|
109
|
+
}
|
|
110
|
+
if (entry.args !== undefined && (!Array.isArray(entry.args) || entry.args.some((arg) => typeof arg !== 'string'))) {
|
|
111
|
+
throw new Error('The dsh MCP command args must be an array of strings. Rerun dsh-mcp setup.');
|
|
112
|
+
}
|
|
113
|
+
if (entry.env !== undefined && (!entry.env || typeof entry.env !== 'object' || Array.isArray(entry.env)
|
|
114
|
+
|| Object.values(entry.env).some((value) => typeof value !== 'string'))) {
|
|
115
|
+
throw new Error('The dsh MCP env entries must be strings. Check the Codex config.');
|
|
116
|
+
}
|
|
117
|
+
if (entry.env_vars !== undefined && (!Array.isArray(entry.env_vars) || entry.env_vars.some((name) => typeof name !== 'string'))) {
|
|
118
|
+
throw new Error('The dsh MCP env_vars must be an array of variable names. Check the Codex config.');
|
|
119
|
+
}
|
|
120
|
+
if (entry.cwd !== undefined && (typeof entry.cwd !== 'string' || !isAbsolute(entry.cwd))) {
|
|
121
|
+
throw new Error('The dsh MCP cwd must be an absolute directory path. Rerun dsh-mcp setup.');
|
|
122
|
+
}
|
|
123
|
+
const cwd = entry.cwd || process.cwd();
|
|
124
|
+
try {
|
|
125
|
+
if (!(await stat(cwd)).isDirectory()) throw new Error();
|
|
126
|
+
} catch {
|
|
127
|
+
throw new Error('The dsh MCP working directory is missing or inaccessible. Fix cwd or rerun setup.');
|
|
128
|
+
}
|
|
129
|
+
const env = { ...getDefaultEnvironment() };
|
|
130
|
+
for (const name of entry.env_vars || []) {
|
|
131
|
+
if (process.env[name] !== undefined) env[name] = process.env[name];
|
|
132
|
+
}
|
|
133
|
+
Object.assign(env, entry.env || {});
|
|
134
|
+
const command = await executablePath(entry.command, env, cwd);
|
|
135
|
+
if (!command) {
|
|
136
|
+
result.hints.push(pathHint(entry.command));
|
|
137
|
+
throw new Error('The registered dsh executable is missing or not executable. Check its absolute path and the client PATH.');
|
|
138
|
+
}
|
|
139
|
+
if (/^node(?:js)?(?:\.exe)?$/.test(basename(command))) {
|
|
140
|
+
const args = entry.args || [];
|
|
141
|
+
if (!args.some((arg) => ['-e', '--eval', '-p', '--print'].includes(arg))) {
|
|
142
|
+
const script = args.find((arg) => !arg.startsWith('-') && /\.[cm]?js$/.test(arg));
|
|
143
|
+
if (script) {
|
|
144
|
+
try {
|
|
145
|
+
const path = resolve(cwd, script);
|
|
146
|
+
await access(path, constants.R_OK);
|
|
147
|
+
if (!(await stat(path)).isFile()) throw new Error();
|
|
148
|
+
} catch {
|
|
149
|
+
result.hints.push(pathHint(script));
|
|
150
|
+
throw new Error('The registered dsh server script is missing or unreadable. Its source checkout or package cache may have moved.');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (checkCodexCli) result.codexCli = await inspectCodexCli(entry, result.configPath, timeoutMs);
|
|
156
|
+
client = new Client({ name: 'dsh-mcp-doctor', version: '1' });
|
|
157
|
+
transport = new StdioClientTransport({ command, args: entry.args || [], cwd, env, stderr: 'pipe' });
|
|
158
|
+
transport.stderr.on('data', (chunk) => {
|
|
159
|
+
if (stderr.length < 8192) stderr += chunk.toString().slice(0, 8192 - stderr.length);
|
|
160
|
+
});
|
|
161
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
162
|
+
await client.connect(transport, { signal, timeout: timeoutMs });
|
|
163
|
+
const version = client.getServerVersion()?.version;
|
|
164
|
+
result.serverVersion = typeof version === 'string' && /^[\w.+-]{1,100}$/.test(version) ? version : null;
|
|
165
|
+
const names = new Set();
|
|
166
|
+
const cursors = new Set();
|
|
167
|
+
let cursor;
|
|
168
|
+
do {
|
|
169
|
+
const page = await client.listTools(cursor ? { cursor } : undefined, { signal, timeout: timeoutMs });
|
|
170
|
+
for (const tool of page.tools) {
|
|
171
|
+
if (/^[\w.-]{1,128}$/.test(tool.name)) names.add(tool.name);
|
|
172
|
+
}
|
|
173
|
+
cursor = page.nextCursor;
|
|
174
|
+
if (cursor && cursors.has(cursor)) throw new Error('MCP tools/list repeated a pagination cursor.');
|
|
175
|
+
if (cursor) cursors.add(cursor);
|
|
176
|
+
} while (cursor);
|
|
177
|
+
result.tools = [...names].sort();
|
|
178
|
+
if (!names.has(requiredTool)) throw new Error('The MCP server initialized but did not expose delegate_to_deepseek. Rerun setup using the current dsh-mcp package.');
|
|
179
|
+
if (result.codexCli?.ok === false) throw new Error(result.codexCli.error);
|
|
180
|
+
result.ok = true;
|
|
181
|
+
result.hints.push(reloadHint);
|
|
182
|
+
if (result.codexCli?.available === false) result.hints.push('The global codex CLI is not installed or not on PATH. This does not prevent a healthy SDK/Desktop stdio installation.');
|
|
183
|
+
} catch (error) {
|
|
184
|
+
result.error = client
|
|
185
|
+
? `MCP initialize/tools discovery failed (${startupDetail(error, stderr)}).`
|
|
186
|
+
: error.message;
|
|
187
|
+
if (client && /did not expose|pagination cursor|Codex CLI/.test(error.message)) result.error = error.message;
|
|
188
|
+
result.hints.push('After correcting registration/startup errors, rerun dsh-mcp doctor. A saved config file alone does not prove Codex loaded the server.');
|
|
189
|
+
if (client) result.hints.push('Check the registered package dependencies and runtime version; if its npm cache was removed, rerun dsh-mcp setup from the current package.');
|
|
190
|
+
} finally {
|
|
191
|
+
if (transport) await transport.close().catch(() => {});
|
|
192
|
+
if (client) await client.close().catch(() => {});
|
|
193
|
+
}
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function runDoctor() {
|
|
198
|
+
const { values } = parseArgs({
|
|
199
|
+
options: {
|
|
200
|
+
home: { type: 'string' }, 'codex-home': { type: 'string' },
|
|
201
|
+
json: { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
if (values.help) {
|
|
205
|
+
console.log('Usage: dsh-mcp doctor [--codex-home PATH] [--home PATH] [--json]\n\nCheck the registered stdio command with MCP initialize and tools/list, without an inference request.\nDefaults to $CODEX_HOME/config.toml or ~/.codex/config.toml. --home checks the registered --dsh-home.\nThis does not inspect or restart the running Codex Desktop connection.');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const configPath = join(resolve(values['codex-home'] || process.env.CODEX_HOME || join(homedir(), '.codex')), 'config.toml');
|
|
209
|
+
const result = await inspectRegistration({ configPath });
|
|
210
|
+
if (values.home && result.ok) {
|
|
211
|
+
const entry = parse(await readFile(configPath, 'utf8')).mcp_servers.dsh;
|
|
212
|
+
const index = (entry.args || []).indexOf('--dsh-home');
|
|
213
|
+
if (index < 0 || !entry.args[index + 1] || resolve(entry.cwd || process.cwd(), entry.args[index + 1]) !== resolve(values.home)) {
|
|
214
|
+
result.ok = false;
|
|
215
|
+
result.error = 'The registered --dsh-home differs from --home. Rerun setup with the intended --home and --codex-home.';
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (values.json) console.log(JSON.stringify(result, null, 2));
|
|
219
|
+
else {
|
|
220
|
+
console.log(`Codex config: ${result.configPath}`);
|
|
221
|
+
console.log(`MCP registration: ${result.ok ? 'healthy (initialize + tools/list succeeded)' : 'unhealthy'}`);
|
|
222
|
+
if (result.serverVersion) console.log(`Server version: ${result.serverVersion}`);
|
|
223
|
+
console.log(`Tools: ${result.tools.join(', ') || '(none discovered)'}`);
|
|
224
|
+
if (result.codexCli) console.log(`Codex CLI effective config: ${!result.codexCli.available ? 'not installed (optional)' : result.codexCli.ok ? 'matches' : 'failed'}`);
|
|
225
|
+
if (result.error) console.log(`Error: ${result.error}`);
|
|
226
|
+
for (const hint of result.hints) console.log(`- ${hint}`);
|
|
227
|
+
}
|
|
228
|
+
if (!result.ok) process.exitCode = 1;
|
|
229
|
+
return result;
|
|
230
|
+
}
|