dsh-deeptutor 0.2.1 → 0.3.1
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/deeptutor/README.md +17 -6
- package/.agents/skills/deeptutor/SKILL.md +91 -126
- package/README.md +85 -19
- package/README.zh.md +55 -10
- package/lib/cli-exec.d.ts +10 -4
- package/lib/cli-exec.js +9 -1
- package/lib/cli-policy.d.ts +23 -0
- package/lib/cli-policy.js +86 -0
- package/lib/config.d.ts +0 -1
- package/lib/config.js +0 -9
- package/lib/discovery.d.ts +45 -0
- package/lib/discovery.js +96 -0
- package/lib/index.d.ts +4 -3
- package/lib/index.js +21 -283
- package/lib/mcp-main.d.ts +2 -0
- package/lib/mcp-main.js +7 -0
- package/lib/mcp.d.ts +25 -0
- package/lib/mcp.js +133 -0
- package/lib/tools.d.ts +52 -0
- package/lib/tools.js +304 -0
- package/package.json +9 -5
- package/scripts/check-cli.ts +80 -0
- package/scripts/mcp-launch.mjs +71 -0
- package/src/cli-exec.ts +21 -9
- package/src/cli-policy.ts +99 -0
- package/src/config.ts +0 -10
- package/src/discovery.ts +112 -0
- package/src/index.ts +30 -317
- package/src/mcp-main.ts +8 -0
- package/src/mcp.ts +151 -0
- package/src/tools.ts +349 -0
package/src/discovery.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepTutor bridge: runtime discovery of the capabilities a deployment offers.
|
|
3
|
+
*
|
|
4
|
+
* DeepTutor 1.7 ships capabilities as optional modules, so the set differs per
|
|
5
|
+
* deployment and grows with every release. Instead of a hard-coded enum the
|
|
6
|
+
* plugin asks `deeptutor module list` (JSON) which modules are ready and what
|
|
7
|
+
* capabilities they provide, and caches the answer for a few minutes.
|
|
8
|
+
*
|
|
9
|
+
* Discovery is advisory: a capability that no module lists may still come from a
|
|
10
|
+
* DeepTutor plugin, so an unknown name is passed through to the CLI. Only a
|
|
11
|
+
* capability whose module is known but not ready is refused up front, because
|
|
12
|
+
* that run is certain to fail and the reason is worth telling the agent.
|
|
13
|
+
*/
|
|
14
|
+
import { runCli, type CliResult } from "./cli-exec.ts";
|
|
15
|
+
|
|
16
|
+
export interface ModuleInfo {
|
|
17
|
+
id: string;
|
|
18
|
+
ready: boolean;
|
|
19
|
+
capabilities: string[];
|
|
20
|
+
issues: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CapabilityIndex {
|
|
24
|
+
/** Capabilities whose module is installed, enabled and ready. */
|
|
25
|
+
ready: string[];
|
|
26
|
+
/** Capabilities whose module exists but is not ready → module id + reason. */
|
|
27
|
+
unavailable: Record<string, { module: string; reason: string }>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Parse `deeptutor module list` output (a JSON array). Tolerates leading noise. Pure. */
|
|
31
|
+
export function parseModuleList(stdout: string): ModuleInfo[] | undefined {
|
|
32
|
+
const start = stdout.indexOf("[");
|
|
33
|
+
if (start < 0) return undefined;
|
|
34
|
+
let raw: unknown;
|
|
35
|
+
try {
|
|
36
|
+
raw = JSON.parse(stdout.slice(start));
|
|
37
|
+
} catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
if (!Array.isArray(raw)) return undefined;
|
|
41
|
+
const out: ModuleInfo[] = [];
|
|
42
|
+
for (const m of raw) {
|
|
43
|
+
if (!m || typeof m !== "object" || typeof (m as any).id !== "string") continue;
|
|
44
|
+
const r = m as Record<string, unknown>;
|
|
45
|
+
out.push({
|
|
46
|
+
id: r.id as string,
|
|
47
|
+
ready: r.installed === true && r.enabled === true && r.ready === true,
|
|
48
|
+
capabilities: Array.isArray(r.capabilities)
|
|
49
|
+
? r.capabilities.filter((c): c is string => typeof c === "string")
|
|
50
|
+
: [],
|
|
51
|
+
issues: Array.isArray(r.issues) ? r.issues.map((i) => String(i)) : [],
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Fold module infos into a capability index. Pure. */
|
|
58
|
+
export function indexCapabilities(modules: ModuleInfo[]): CapabilityIndex {
|
|
59
|
+
const ready = new Set<string>();
|
|
60
|
+
const unavailable: CapabilityIndex["unavailable"] = {};
|
|
61
|
+
for (const m of modules) {
|
|
62
|
+
for (const cap of m.capabilities) {
|
|
63
|
+
if (m.ready) ready.add(cap);
|
|
64
|
+
else
|
|
65
|
+
unavailable[cap] = {
|
|
66
|
+
module: m.id,
|
|
67
|
+
reason: m.issues.length ? m.issues.join("; ") : "module not installed or not enabled",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (const cap of ready) delete unavailable[cap];
|
|
72
|
+
return { ready: [...ready].sort(), unavailable };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const CACHE_MS = 5 * 60_000;
|
|
76
|
+
let cache: { at: number; index: CapabilityIndex } | undefined;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Current capability index, cached for 5 minutes. Returns undefined when the
|
|
80
|
+
* deployment cannot tell (old DeepTutor without `module list`, CLI unreachable).
|
|
81
|
+
*/
|
|
82
|
+
export async function discoverCapabilities(
|
|
83
|
+
signal?: AbortSignal,
|
|
84
|
+
run: (args: string[], opts: { signal?: AbortSignal; timeoutMs?: number }) => Promise<CliResult> = runCli,
|
|
85
|
+
now: () => number = Date.now,
|
|
86
|
+
): Promise<CapabilityIndex | undefined> {
|
|
87
|
+
if (cache && now() - cache.at < CACHE_MS) return cache.index;
|
|
88
|
+
const res = await run(["module", "list"], { signal, timeoutMs: 60_000 });
|
|
89
|
+
if (res.code !== 0) return undefined;
|
|
90
|
+
const modules = parseModuleList(res.stdout);
|
|
91
|
+
if (!modules) return undefined;
|
|
92
|
+
const index = indexCapabilities(modules);
|
|
93
|
+
cache = { at: now(), index };
|
|
94
|
+
return index;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Test hook: forget the cached index. */
|
|
98
|
+
export function resetCapabilityCache() {
|
|
99
|
+
cache = undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** One line listing the ready capabilities, for tool results and errors. */
|
|
103
|
+
export function describeCapabilities(index: CapabilityIndex | undefined): string {
|
|
104
|
+
if (!index) return "Available capabilities: unknown (run `deeptutor module list` to check).";
|
|
105
|
+
const off = Object.entries(index.unavailable)
|
|
106
|
+
.map(([cap, u]) => `${cap} (module ${u.module}: ${u.reason})`)
|
|
107
|
+
.join(", ");
|
|
108
|
+
return (
|
|
109
|
+
`Available capabilities: ${index.ready.join(", ") || "(none)"}` +
|
|
110
|
+
(off ? `\nNot ready on this deployment: ${off}` : "")
|
|
111
|
+
);
|
|
112
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DeepTutor bridge plugin for DeepSeek Harness (dsh).
|
|
3
3
|
*
|
|
4
|
-
* Registers
|
|
4
|
+
* Registers the host-neutral tools from src/tools.ts:
|
|
5
5
|
* - deeptutor_run — execute a DeepTutor learning capability
|
|
6
6
|
* - deeptutor_kb — list / search / info knowledge bases
|
|
7
7
|
* - deeptutor_note — archive a Markdown learning note to a server notebook
|
|
8
|
+
* - deeptutor_cli — any other DeepTutor CLI command, behind safety tiers
|
|
9
|
+
*
|
|
10
|
+
* The same tools are served to MCP clients (Claude Code, …) by src/mcp.ts.
|
|
8
11
|
*
|
|
9
12
|
* Transport: every tool drives the deeptutor CLI.
|
|
10
13
|
* - Local: DEEPTUTOR_SSH_HOST unset → runs the local CLI (DEEPTUTOR_LOCAL_BIN).
|
|
11
14
|
* - Remote: DEEPTUTOR_SSH_HOST set → runs the server's CLI over SSH.
|
|
12
|
-
* The HTTP/WS API was dropped: DeepTutor 1.7 moved it off /api/v1 and put it behind
|
|
13
|
-
* a login, so the old probe always 404'd and cost ~30s per call before the CLI ran anyway.
|
|
14
15
|
*
|
|
15
16
|
* Env config (agent-agnostic): DEEPTUTOR_SSH_HOST / DEEPTUTOR_LOCAL_BIN /
|
|
16
17
|
* DEEPTUTOR_REMOTE_BIN / DEEPTUTOR_REMOTE_HOME — see src/config.ts and
|
|
@@ -18,329 +19,41 @@
|
|
|
18
19
|
*/
|
|
19
20
|
import type { Context } from "@deepseek-ai/cordis";
|
|
20
21
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
21
|
-
import { CAPABILITIES, configGuide, configHint, SSH_HOST, truncate } from "./config.ts";
|
|
22
|
-
import { cliNoteLocal, cliNoteRemote, runCli } from "./cli-exec.ts";
|
|
23
|
-
import { renderHtml } from "./html-render.ts";
|
|
24
|
-
import { cliRunTurn, fmtTurnResult } from "./turn.ts";
|
|
25
22
|
import { HTML_DOC_SCRIPT } from "./html-render.ts";
|
|
23
|
+
import { TOOLS } from "./tools.ts";
|
|
26
24
|
|
|
27
25
|
export const name = "dsh-deeptutor";
|
|
28
26
|
export const inject = ["tools"] as const;
|
|
29
27
|
|
|
30
|
-
/** Drop
|
|
31
|
-
|
|
32
|
-
[K in keyof T as T[K] extends undefined ? never : K]: Exclude<T[K], undefined>;
|
|
33
|
-
};
|
|
34
|
-
function json<T extends Record<string, unknown>>(o: T): Jsonify<T> {
|
|
28
|
+
/** Drop undefined values so the canonical JSON value stays lossless-JSON. */
|
|
29
|
+
function json(o: Record<string, unknown>): { text: string } & Record<string, any> {
|
|
35
30
|
const out: Record<string, unknown> = {};
|
|
36
31
|
for (const [k, v] of Object.entries(o)) if (v !== undefined) out[k] = v;
|
|
37
|
-
return out as
|
|
32
|
+
return out as { text: string };
|
|
38
33
|
}
|
|
39
34
|
|
|
35
|
+
const OUTPUT = {
|
|
36
|
+
schema: {
|
|
37
|
+
type: "object",
|
|
38
|
+
properties: { text: { type: "string" } },
|
|
39
|
+
additionalProperties: true,
|
|
40
|
+
},
|
|
41
|
+
render: (_args: unknown, value: { text?: string }) => [{ type: "text" as const, text: value.text ?? "" }],
|
|
42
|
+
} as const;
|
|
43
|
+
|
|
40
44
|
export function apply(ctx: Context) {
|
|
41
45
|
console.log(`[dsh-deeptutor] plugin loaded (html script: ${HTML_DOC_SCRIPT})`);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
},
|
|
56
|
-
prompt: {
|
|
57
|
-
type: "string",
|
|
58
|
-
required: true,
|
|
59
|
-
description:
|
|
60
|
-
"The learning question or instruction for this turn (e.g. explain the async/await state machine)",
|
|
61
|
-
},
|
|
62
|
-
kbs: {
|
|
63
|
-
type: "array",
|
|
64
|
-
items: { type: "string" },
|
|
65
|
-
description:
|
|
66
|
-
"Knowledge base names (check deeptutor_kb list for available ones, e.g. the user's dotnet/sqlserver bases)",
|
|
67
|
-
},
|
|
68
|
-
tools: {
|
|
69
|
-
type: "array",
|
|
70
|
-
items: { type: "string" },
|
|
71
|
-
description:
|
|
72
|
-
"Tools to enable: rag (KB retrieval), web_search, reason, brainstorm, paper_search, code_execution, read_source, etc.",
|
|
73
|
-
},
|
|
74
|
-
session_id: {
|
|
75
|
-
type: "string",
|
|
76
|
-
description:
|
|
77
|
-
"Session ID from a previous deeptutor_run call to continue that context",
|
|
78
|
-
},
|
|
79
|
-
config: {
|
|
80
|
-
type: "object",
|
|
81
|
-
additionalProperties: true,
|
|
82
|
-
description:
|
|
83
|
-
"Capability config key-values, e.g. num_questions=5 for deep_question, mode=report / depth=standard for deep_research",
|
|
84
|
-
},
|
|
85
|
-
language: {
|
|
86
|
-
type: "string",
|
|
87
|
-
description: "Response language code, default en; use zh for Chinese",
|
|
88
|
-
},
|
|
89
|
-
timeout: {
|
|
90
|
-
type: "number",
|
|
91
|
-
description: "Timeout in seconds, default 600; use 900+ for deep_research",
|
|
92
|
-
},
|
|
93
|
-
max_chars: {
|
|
94
|
-
type: "number",
|
|
95
|
-
description:
|
|
96
|
-
"Max characters of the answer returned to the model, default 30000",
|
|
97
|
-
},
|
|
98
|
-
html: {
|
|
99
|
-
type: "string",
|
|
100
|
-
description:
|
|
101
|
-
"Optional output path for a self-contained local HTML render of the answer (bundled md-to-html converter; also writes the companion .md source next to it). e.g. data/study/abstract-class/knowledge.html — open the HTML in a browser to read comfortably. Set it when the user wants browsable/printable study material.",
|
|
102
|
-
},
|
|
103
|
-
html_title: {
|
|
104
|
-
type: "string",
|
|
105
|
-
description:
|
|
106
|
-
"HTML page title (default: derived from the html output file name)",
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
output: {
|
|
110
|
-
schema: {
|
|
111
|
-
type: "object",
|
|
112
|
-
properties: {
|
|
113
|
-
mode: { type: "string" },
|
|
114
|
-
sessionId: { type: "string" },
|
|
115
|
-
answer: { type: "string" },
|
|
116
|
-
toolCalls: { type: "array", items: { type: "string" } },
|
|
117
|
-
errors: { type: "array", items: { type: "string" } },
|
|
118
|
-
text: { type: "string" },
|
|
119
|
-
},
|
|
120
|
-
additionalProperties: true,
|
|
121
|
-
},
|
|
122
|
-
render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
|
|
123
|
-
},
|
|
124
|
-
async execute(args, exec) {
|
|
125
|
-
const timeoutMs = (args.timeout ?? 600) * 1000;
|
|
126
|
-
const maxChars = args.max_chars ?? 30000;
|
|
127
|
-
try {
|
|
128
|
-
const r = await cliRunTurn(args, timeoutMs, exec.signal);
|
|
129
|
-
const htmlNote = await renderHtml(args, r.answer);
|
|
130
|
-
return json({
|
|
131
|
-
mode: "cli",
|
|
132
|
-
sessionId: r.sessionId,
|
|
133
|
-
toolCalls: r.toolCalls,
|
|
134
|
-
errors: r.errors,
|
|
135
|
-
text:
|
|
136
|
-
configHint() + fmtTurnResult(args.capability, r, maxChars) + htmlNote,
|
|
137
|
-
});
|
|
138
|
-
} catch (err: any) {
|
|
139
|
-
if (exec.signal.aborted) {
|
|
140
|
-
// user cancelled mid-CLI-run: report it as a cancellation, not a failure
|
|
141
|
-
throw new Error("[deeptutor_run cancelled]");
|
|
142
|
-
}
|
|
143
|
-
throw new Error(
|
|
144
|
-
configGuide() +
|
|
145
|
-
`[deeptutor_run failed] ${err?.message ?? String(err)}`,
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
},
|
|
149
|
-
}),
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
// ---------- Tool 2: knowledge base operations ----------
|
|
153
|
-
ctx.tools.register(
|
|
154
|
-
defineTool({
|
|
155
|
-
name: "deeptutor_kb",
|
|
156
|
-
description:
|
|
157
|
-
"Manage/search DeepTutor knowledge bases: list all bases, search inside a base (RAG hit snippets), info for details. Works in both local and remote deployments. Search the user's personal knowledge bases before generating learning content and combine the hits with authoritative sources.",
|
|
158
|
-
parameters: {
|
|
159
|
-
action: {
|
|
160
|
-
type: "string",
|
|
161
|
-
enum: ["list", "search", "info"],
|
|
162
|
-
required: true,
|
|
163
|
-
description: "What to do with the knowledge bases",
|
|
164
|
-
},
|
|
165
|
-
kb: {
|
|
166
|
-
type: "string",
|
|
167
|
-
description: "Knowledge base name (required for search/info)",
|
|
168
|
-
},
|
|
169
|
-
query: {
|
|
170
|
-
type: "string",
|
|
171
|
-
description:
|
|
172
|
-
"Search question (required for search, e.g. C# generics covariance/contravariance)",
|
|
173
|
-
},
|
|
174
|
-
mode: {
|
|
175
|
-
type: "string",
|
|
176
|
-
enum: ["hybrid", "dense", "sparse"],
|
|
177
|
-
description: "Retrieval mode, default hybrid",
|
|
178
|
-
},
|
|
179
|
-
top_k: {
|
|
180
|
-
type: "number",
|
|
181
|
-
description:
|
|
182
|
-
"Number of hits to return, default 5 (applied locally to the CLI search result; the deeptutor CLI has no --top-k flag)",
|
|
183
|
-
},
|
|
184
|
-
max_chars: {
|
|
185
|
-
type: "number",
|
|
186
|
-
description:
|
|
187
|
-
"Max characters of the returned content, default 30000",
|
|
188
|
-
},
|
|
189
|
-
},
|
|
190
|
-
output: {
|
|
191
|
-
schema: {
|
|
192
|
-
type: "object",
|
|
193
|
-
properties: {
|
|
194
|
-
action: { type: "string" },
|
|
195
|
-
mode: { type: "string" },
|
|
196
|
-
kb: { type: "string" },
|
|
197
|
-
query: { type: "string" },
|
|
198
|
-
text: { type: "string" },
|
|
199
|
-
},
|
|
200
|
-
additionalProperties: true,
|
|
201
|
-
},
|
|
202
|
-
render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
|
|
203
|
-
},
|
|
204
|
-
async execute(args, exec) {
|
|
205
|
-
const maxChars = args.max_chars ?? 30000;
|
|
206
|
-
// Local or SSH CLI. NOTE: `kb search` has no --top-k flag
|
|
207
|
-
// (verified on deeptutor CLI 1.5.x); top_k is applied locally below.
|
|
208
|
-
let argv: string[];
|
|
209
|
-
if (args.action === "list") {
|
|
210
|
-
argv = ["kb", "list", "--format", "json"];
|
|
211
|
-
} else if (args.action === "info") {
|
|
212
|
-
argv = ["kb", "info", args.kb ?? ""];
|
|
213
|
-
} else {
|
|
214
|
-
argv = [
|
|
215
|
-
"kb",
|
|
216
|
-
"search",
|
|
217
|
-
args.kb ?? "",
|
|
218
|
-
args.query ?? "",
|
|
219
|
-
"--format",
|
|
220
|
-
"json",
|
|
221
|
-
"--mode",
|
|
222
|
-
args.mode ?? "hybrid",
|
|
223
|
-
];
|
|
224
|
-
}
|
|
225
|
-
const { stdout, stderr, code, killed } = await runCli(argv, {
|
|
226
|
-
signal: exec.signal,
|
|
227
|
-
timeoutMs: 90_000,
|
|
228
|
-
});
|
|
229
|
-
if (killed) throw new Error(configGuide() + "[deeptutor_kb timed out]");
|
|
230
|
-
if (code !== 0) {
|
|
231
|
-
throw new Error(
|
|
232
|
-
`[deeptutor_kb failed] exit=${code}\nstderr: ${truncate(stderr || "(none)", 2000)}` +
|
|
233
|
-
configGuide(),
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
let body = stdout.trim() || "(empty result)";
|
|
237
|
-
if (args.action === "search") {
|
|
238
|
-
try {
|
|
239
|
-
const parsed = JSON.parse(stdout);
|
|
240
|
-
const fmtHit = (s: any, i: number) => {
|
|
241
|
-
const text = s.text ?? s.content ?? s.snippet ?? JSON.stringify(s);
|
|
242
|
-
const src = s.source ?? s.doc_name ?? s.file ?? "";
|
|
243
|
-
const title = s.title ? ` title: ${s.title}` : "";
|
|
244
|
-
const score = typeof s.score === "number" ? ` score: ${s.score}` : "";
|
|
245
|
-
return `[${i + 1}]${src ? ` source: ${src}` : ""}${title}${score}\n${typeof text === "string" ? text : JSON.stringify(text)}`;
|
|
246
|
-
};
|
|
247
|
-
// Current deeptutor CLI (1.5.x) returns an object
|
|
248
|
-
// { query, answer, content, sources, provider }; older builds
|
|
249
|
-
// returned a bare array of hits. Accept both.
|
|
250
|
-
if (Array.isArray(parsed)) {
|
|
251
|
-
const hits = args.top_k ? parsed.slice(0, args.top_k) : parsed;
|
|
252
|
-
body = `${hits.length} hits:\n\n` + hits.map(fmtHit).join("\n\n");
|
|
253
|
-
} else if (parsed && typeof parsed === "object") {
|
|
254
|
-
const parts: string[] = [];
|
|
255
|
-
if (parsed.query) parts.push(`Query: ${parsed.query}`);
|
|
256
|
-
if (typeof parsed.answer === "string" && parsed.answer.trim())
|
|
257
|
-
parts.push(`Answer: ${parsed.answer.trim()}`);
|
|
258
|
-
const sources = Array.isArray(parsed.sources) ? parsed.sources : [];
|
|
259
|
-
const hits = args.top_k ? sources.slice(0, args.top_k) : sources;
|
|
260
|
-
parts.push(`${hits.length} hits:`);
|
|
261
|
-
parts.push(hits.map(fmtHit).join("\n\n"));
|
|
262
|
-
body = parts.join("\n\n");
|
|
263
|
-
}
|
|
264
|
-
} catch {
|
|
265
|
-
/* non-JSON output returned as-is */
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
return json({
|
|
269
|
-
action: args.action,
|
|
270
|
-
mode: "cli",
|
|
271
|
-
kb: args.kb,
|
|
272
|
-
query: args.query,
|
|
273
|
-
text: truncate(body, maxChars),
|
|
274
|
-
});
|
|
275
|
-
},
|
|
276
|
-
}),
|
|
277
|
-
);
|
|
278
|
-
|
|
279
|
-
// ---------- Tool 3: archive learning notes to a server notebook ----------
|
|
280
|
-
ctx.tools.register(
|
|
281
|
-
defineTool({
|
|
282
|
-
name: "deeptutor_note",
|
|
283
|
-
description:
|
|
284
|
-
"Archive a Markdown learning note (study plan, deep-solve summary, wrong answers, etc.) to a DeepTutor notebook (auto-creates the notebook if missing). Record type: chat / question / research / solve.",
|
|
285
|
-
parameters: {
|
|
286
|
-
notebook: {
|
|
287
|
-
type: "string",
|
|
288
|
-
required: true,
|
|
289
|
-
description:
|
|
290
|
-
"Notebook name, e.g. dotnet-learning (auto-created if missing)",
|
|
291
|
-
},
|
|
292
|
-
title: {
|
|
293
|
-
type: "string",
|
|
294
|
-
required: true,
|
|
295
|
-
description: "Record title, e.g. async/await state machine deep-dive notes",
|
|
296
|
-
},
|
|
297
|
-
type: {
|
|
298
|
-
type: "string",
|
|
299
|
-
enum: ["chat", "question", "research", "solve"],
|
|
300
|
-
description: "Record type, default chat",
|
|
301
|
-
},
|
|
302
|
-
content: {
|
|
303
|
-
type: "string",
|
|
304
|
-
required: true,
|
|
305
|
-
description: "Learning note content in Markdown",
|
|
306
|
-
},
|
|
307
|
-
},
|
|
308
|
-
output: {
|
|
309
|
-
schema: {
|
|
310
|
-
type: "object",
|
|
311
|
-
properties: {
|
|
312
|
-
notebook: { type: "string" },
|
|
313
|
-
nbId: { type: "string" },
|
|
314
|
-
title: { type: "string" },
|
|
315
|
-
mode: { type: "string" },
|
|
316
|
-
text: { type: "string" },
|
|
317
|
-
},
|
|
318
|
-
additionalProperties: true,
|
|
319
|
-
},
|
|
320
|
-
render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
|
|
321
|
-
},
|
|
322
|
-
async execute(args, exec) {
|
|
323
|
-
const recordType = (args.type ?? "chat") as string;
|
|
324
|
-
const nbName = args.notebook.trim();
|
|
325
|
-
// Remote deployment runs the PY_NOTE script on the server; local deployment runs locally
|
|
326
|
-
const { stdout, stderr, code } = SSH_HOST
|
|
327
|
-
? await cliNoteRemote(nbName, args.title, recordType, args.content, exec.signal)
|
|
328
|
-
: await cliNoteLocal(nbName, args.title, recordType, args.content, exec.signal);
|
|
329
|
-
if (code !== 0) {
|
|
330
|
-
throw new Error(
|
|
331
|
-
`[deeptutor_note failed] exit=${code}\n${truncate(stderr || stdout, 2000)}` +
|
|
332
|
-
configGuide(),
|
|
333
|
-
);
|
|
334
|
-
}
|
|
335
|
-
return json({
|
|
336
|
-
notebook: nbName,
|
|
337
|
-
title: args.title,
|
|
338
|
-
mode: "cli",
|
|
339
|
-
text:
|
|
340
|
-
configHint() +
|
|
341
|
-
`[note archived] notebook=${nbName}, title=${args.title}, type=${recordType}\n${truncate((stdout ?? "").trim(), 2000)}`,
|
|
342
|
-
});
|
|
343
|
-
},
|
|
344
|
-
}),
|
|
345
|
-
);
|
|
46
|
+
for (const tool of TOOLS) {
|
|
47
|
+
ctx.tools.register(
|
|
48
|
+
defineTool({
|
|
49
|
+
name: tool.name,
|
|
50
|
+
description: tool.description,
|
|
51
|
+
parameters: tool.parameters,
|
|
52
|
+
output: OUTPUT,
|
|
53
|
+
async execute(args, exec) {
|
|
54
|
+
return json(await tool.execute(args as Record<string, any>, exec.signal)) as any;
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
346
59
|
}
|
package/src/mcp-main.ts
ADDED
package/src/mcp.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepTutor bridge: Model Context Protocol server over stdio.
|
|
3
|
+
*
|
|
4
|
+
* Serves the same tools as the dsh plugin (src/tools.ts) to MCP clients such as
|
|
5
|
+
* Claude Code. Hand-rolled on purpose: the protocol surface a tools-only server
|
|
6
|
+
* needs is small (initialize, ping, tools/list, tools/call, cancellation), and
|
|
7
|
+
* keeping it in-house preserves the package's zero runtime dependencies.
|
|
8
|
+
*
|
|
9
|
+
* Transport: newline-delimited JSON-RPC 2.0 on stdin/stdout. stdout carries
|
|
10
|
+
* protocol messages only; diagnostics go to stderr.
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
import { createInterface } from "node:readline";
|
|
14
|
+
import { TOOLS, toJsonSchema, validateArgs, type ToolSpec } from "./tools.ts";
|
|
15
|
+
|
|
16
|
+
const SUPPORTED_PROTOCOLS = ["2024-11-05", "2025-03-26", "2025-06-18"];
|
|
17
|
+
const LATEST_PROTOCOL = SUPPORTED_PROTOCOLS[SUPPORTED_PROTOCOLS.length - 1];
|
|
18
|
+
|
|
19
|
+
const INSTRUCTIONS =
|
|
20
|
+
"DeepTutor learning tools. Explain topics with deeptutor_run (deep_solve, deep_question, …), " +
|
|
21
|
+
"search the user's knowledge bases with deeptutor_kb before answering from memory, archive notes " +
|
|
22
|
+
"with deeptutor_note only on request, and reach every other DeepTutor CLI command through " +
|
|
23
|
+
"deeptutor_cli, discovering commands with args=[\"--help\"] rather than guessing.";
|
|
24
|
+
|
|
25
|
+
function packageVersion(): string {
|
|
26
|
+
try {
|
|
27
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
28
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
29
|
+
} catch {
|
|
30
|
+
return "0.0.0";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type Id = string | number;
|
|
35
|
+
interface Message {
|
|
36
|
+
jsonrpc?: string;
|
|
37
|
+
id?: Id | null;
|
|
38
|
+
method?: string;
|
|
39
|
+
params?: any;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface McpServerOptions {
|
|
43
|
+
tools?: readonly ToolSpec[];
|
|
44
|
+
/** Writes one serialized message (without the trailing newline). */
|
|
45
|
+
send: (line: string) => void;
|
|
46
|
+
log?: (msg: string) => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Transport-free MCP message handler. `handle` takes one parsed message and
|
|
51
|
+
* resolves once any response has been sent. Exported for tests.
|
|
52
|
+
*/
|
|
53
|
+
export function createMcpHandler(opts: McpServerOptions) {
|
|
54
|
+
const tools = opts.tools ?? TOOLS;
|
|
55
|
+
const byName = new Map(tools.map((t) => [t.name, t]));
|
|
56
|
+
const inflight = new Map<Id, AbortController>();
|
|
57
|
+
const log = opts.log ?? (() => {});
|
|
58
|
+
const reply = (id: Id, result: unknown) => opts.send(JSON.stringify({ jsonrpc: "2.0", id, result }));
|
|
59
|
+
const fail = (id: Id | null, code: number, message: string) =>
|
|
60
|
+
opts.send(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }));
|
|
61
|
+
|
|
62
|
+
async function callTool(id: Id, params: any) {
|
|
63
|
+
const tool = byName.get(params?.name);
|
|
64
|
+
if (!tool) return fail(id, -32602, `Unknown tool: ${String(params?.name)}`);
|
|
65
|
+
const args = params?.arguments && typeof params.arguments === "object" ? params.arguments : {};
|
|
66
|
+
const problems = validateArgs(tool.parameters, args);
|
|
67
|
+
if (problems.length) {
|
|
68
|
+
return reply(id, { content: [{ type: "text", text: `[${tool.name}] ${problems.join("; ")}` }], isError: true });
|
|
69
|
+
}
|
|
70
|
+
const ac = new AbortController();
|
|
71
|
+
inflight.set(id, ac);
|
|
72
|
+
try {
|
|
73
|
+
const { text, ...data } = await tool.execute(args, ac.signal);
|
|
74
|
+
const structured = Object.fromEntries(Object.entries(data).filter(([, v]) => v !== undefined));
|
|
75
|
+
if (ac.signal.aborted) return; // cancelled: the client no longer expects a response
|
|
76
|
+
reply(id, { content: [{ type: "text", text }], structuredContent: { text, ...structured } });
|
|
77
|
+
} catch (err: any) {
|
|
78
|
+
if (ac.signal.aborted) return;
|
|
79
|
+
reply(id, { content: [{ type: "text", text: err?.message ?? String(err) }], isError: true });
|
|
80
|
+
} finally {
|
|
81
|
+
inflight.delete(id);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function handle(msg: Message): Promise<void> {
|
|
86
|
+
if (!msg || typeof msg !== "object" || typeof msg.method !== "string") {
|
|
87
|
+
// Responses to requests we never send, or garbage: ignore.
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const isRequest = msg.id !== undefined && msg.id !== null;
|
|
91
|
+
if (!isRequest) {
|
|
92
|
+
if (msg.method === "notifications/cancelled") inflight.get(msg.params?.requestId)?.abort();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const id = msg.id as Id;
|
|
96
|
+
switch (msg.method) {
|
|
97
|
+
case "initialize": {
|
|
98
|
+
const asked = msg.params?.protocolVersion;
|
|
99
|
+
reply(id, {
|
|
100
|
+
protocolVersion: SUPPORTED_PROTOCOLS.includes(asked) ? asked : LATEST_PROTOCOL,
|
|
101
|
+
capabilities: { tools: { listChanged: false } },
|
|
102
|
+
serverInfo: { name: "dsh-deeptutor", version: packageVersion() },
|
|
103
|
+
instructions: INSTRUCTIONS,
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
case "ping":
|
|
108
|
+
reply(id, {});
|
|
109
|
+
return;
|
|
110
|
+
case "tools/list":
|
|
111
|
+
reply(id, {
|
|
112
|
+
tools: tools.map((t) => ({
|
|
113
|
+
name: t.name,
|
|
114
|
+
description: t.description,
|
|
115
|
+
inputSchema: toJsonSchema(t.parameters),
|
|
116
|
+
})),
|
|
117
|
+
});
|
|
118
|
+
return;
|
|
119
|
+
case "tools/call":
|
|
120
|
+
await callTool(id, msg.params);
|
|
121
|
+
return;
|
|
122
|
+
default:
|
|
123
|
+
log(`unsupported method: ${msg.method}`);
|
|
124
|
+
fail(id, -32601, `Method not found: ${msg.method}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { handle, inflight };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Run the server on this process's stdin/stdout until stdin closes. */
|
|
132
|
+
export function startStdioServer() {
|
|
133
|
+
const log = (m: string) => process.stderr.write(`[dsh-deeptutor mcp] ${m}\n`);
|
|
134
|
+
const { handle } = createMcpHandler({ send: (line) => process.stdout.write(line + "\n"), log });
|
|
135
|
+
const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
136
|
+
rl.on("line", (line) => {
|
|
137
|
+
if (!line.trim()) return;
|
|
138
|
+
let msg: Message;
|
|
139
|
+
try {
|
|
140
|
+
msg = JSON.parse(line);
|
|
141
|
+
} catch {
|
|
142
|
+
process.stdout.write(
|
|
143
|
+
JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } }) + "\n",
|
|
144
|
+
);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
handle(msg).catch((err) => log(`handler error: ${err?.stack ?? err}`));
|
|
148
|
+
});
|
|
149
|
+
rl.on("close", () => process.exit(0));
|
|
150
|
+
log(`ready (v${packageVersion()}, ${process.env.DEEPTUTOR_SSH_HOST ? `remote via ${process.env.DEEPTUTOR_SSH_HOST}` : "local CLI"})`);
|
|
151
|
+
}
|