@zhijiewang/openharness 2.9.0 → 2.11.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 +13 -0
- package/dist/harness/config.d.ts +30 -3
- package/dist/harness/hooks.js +158 -7
- package/dist/mcp/client.d.ts +15 -9
- package/dist/mcp/client.js +57 -110
- package/dist/mcp/config-normalize.d.ts +24 -0
- package/dist/mcp/config-normalize.js +72 -0
- package/dist/mcp/loader.js +24 -0
- package/dist/mcp/transport.d.ts +38 -0
- package/dist/mcp/transport.js +159 -0
- package/dist/mcp/types.d.ts +1 -16
- package/dist/mcp/types.js +0 -1
- package/dist/tools/MemoryTool/index.d.ts +2 -2
- package/dist/tools/TaskUpdateTool/index.d.ts +2 -2
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -366,6 +366,19 @@ mcpServers:
|
|
|
366
366
|
|
|
367
367
|
MCP tools appear alongside built-in tools. `/status` shows connected servers.
|
|
368
368
|
|
|
369
|
+
### Remote MCP servers (HTTP / SSE)
|
|
370
|
+
|
|
371
|
+
```yaml
|
|
372
|
+
mcpServers:
|
|
373
|
+
- name: linear
|
|
374
|
+
type: http
|
|
375
|
+
url: https://mcp.linear.app/mcp
|
|
376
|
+
headers:
|
|
377
|
+
Authorization: "Bearer ${LINEAR_API_KEY}"
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
See [docs/mcp-servers.md](docs/mcp-servers.md) for the full reference.
|
|
381
|
+
|
|
369
382
|
**MCP Server Registry** — browse and install from a curated catalog:
|
|
370
383
|
|
|
371
384
|
```
|
package/dist/harness/config.d.ts
CHANGED
|
@@ -2,20 +2,47 @@
|
|
|
2
2
|
* .oh/config.yaml — provider, model, permissionMode and other persisted settings.
|
|
3
3
|
*/
|
|
4
4
|
import type { PermissionMode } from "../types/permissions.js";
|
|
5
|
-
export type
|
|
5
|
+
export type McpCommonConfig = {
|
|
6
6
|
name: string;
|
|
7
|
+
riskLevel?: "low" | "medium" | "high";
|
|
8
|
+
timeout?: number;
|
|
9
|
+
};
|
|
10
|
+
export type McpStdioConfig = McpCommonConfig & {
|
|
11
|
+
type?: "stdio";
|
|
7
12
|
command: string;
|
|
8
13
|
args?: string[];
|
|
9
14
|
env?: Record<string, string>;
|
|
10
|
-
riskLevel?: "low" | "medium" | "high";
|
|
11
|
-
timeout?: number;
|
|
12
15
|
};
|
|
16
|
+
export type McpHttpConfig = McpCommonConfig & {
|
|
17
|
+
type: "http";
|
|
18
|
+
url: string;
|
|
19
|
+
headers?: Record<string, string>;
|
|
20
|
+
};
|
|
21
|
+
export type McpSseConfig = McpCommonConfig & {
|
|
22
|
+
type: "sse";
|
|
23
|
+
url: string;
|
|
24
|
+
headers?: Record<string, string>;
|
|
25
|
+
};
|
|
26
|
+
export type McpServerConfig = McpStdioConfig | McpHttpConfig | McpSseConfig;
|
|
13
27
|
export type HookDef = {
|
|
14
28
|
command?: string;
|
|
15
29
|
http?: string;
|
|
16
30
|
prompt?: string;
|
|
17
31
|
match?: string;
|
|
18
32
|
timeout?: number;
|
|
33
|
+
/**
|
|
34
|
+
* When true (and this hook has a `command`), OH sends a JSON envelope
|
|
35
|
+
* `{event, ...context}` on stdin and parses a JSON response from stdout.
|
|
36
|
+
* Response shape (Claude Code compatible):
|
|
37
|
+
* { "decision": "allow" | "deny",
|
|
38
|
+
* "reason"?: string,
|
|
39
|
+
* "hookSpecificOutput"?: {...} }
|
|
40
|
+
*
|
|
41
|
+
* When false (default), OH passes context via `OH_EVENT` / `OH_TOOL_NAME`
|
|
42
|
+
* env vars and gates on the command's exit code (0 = allow). The env-var
|
|
43
|
+
* mode remains the default for backward compatibility.
|
|
44
|
+
*/
|
|
45
|
+
jsonIO?: boolean;
|
|
19
46
|
};
|
|
20
47
|
export type HooksConfig = {
|
|
21
48
|
sessionStart?: HookDef[];
|
package/dist/harness/hooks.js
CHANGED
|
@@ -141,6 +141,86 @@ function runCommandHookAsync(command, env, timeoutMs = 10_000) {
|
|
|
141
141
|
});
|
|
142
142
|
});
|
|
143
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Run a JSON-mode command hook (Claude Code convention).
|
|
146
|
+
*
|
|
147
|
+
* Sends `{event, ...context}` as JSON on stdin. Parses stdout as JSON
|
|
148
|
+
* `{ decision: "allow" | "deny", reason?: string, hookSpecificOutput?: any }`.
|
|
149
|
+
*
|
|
150
|
+
* Gating logic:
|
|
151
|
+
* - `decision: "deny"` → blocks (returns false).
|
|
152
|
+
* - `decision: "allow"` or omitted decision → allow (returns true).
|
|
153
|
+
* - Non-zero exit code → block.
|
|
154
|
+
* - Invalid/empty JSON on stdout → fall back to exit code (0 = allow).
|
|
155
|
+
* - Timeout or spawn error → block.
|
|
156
|
+
*/
|
|
157
|
+
function runJsonIoHookAsync(command, env, event, ctx, timeoutMs = 10_000) {
|
|
158
|
+
return new Promise((resolve) => {
|
|
159
|
+
const proc = spawn(command, {
|
|
160
|
+
shell: true,
|
|
161
|
+
timeout: timeoutMs,
|
|
162
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
163
|
+
env,
|
|
164
|
+
});
|
|
165
|
+
let settled = false;
|
|
166
|
+
let stdoutBuf = "";
|
|
167
|
+
const timer = setTimeout(() => {
|
|
168
|
+
if (!settled) {
|
|
169
|
+
settled = true;
|
|
170
|
+
proc.kill();
|
|
171
|
+
resolve(false);
|
|
172
|
+
}
|
|
173
|
+
}, timeoutMs);
|
|
174
|
+
proc.stdout?.on("data", (chunk) => {
|
|
175
|
+
stdoutBuf += chunk.toString();
|
|
176
|
+
});
|
|
177
|
+
// Write the event + context JSON envelope to stdin then close it so the
|
|
178
|
+
// hook knows there's no more input coming.
|
|
179
|
+
try {
|
|
180
|
+
const payload = JSON.stringify({ event, ...ctx });
|
|
181
|
+
proc.stdin?.end(payload);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
/* stdin already closed — ignore */
|
|
185
|
+
}
|
|
186
|
+
proc.on("close", (code) => {
|
|
187
|
+
if (settled)
|
|
188
|
+
return;
|
|
189
|
+
settled = true;
|
|
190
|
+
clearTimeout(timer);
|
|
191
|
+
// Non-zero exit is always a block, regardless of stdout.
|
|
192
|
+
if ((code ?? 1) !== 0) {
|
|
193
|
+
resolve(false);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
// Empty stdout → treat exit code as the signal (allow for exit 0).
|
|
197
|
+
if (!stdoutBuf.trim()) {
|
|
198
|
+
resolve(true);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
const parsed = JSON.parse(stdoutBuf);
|
|
203
|
+
if (parsed.decision === "deny") {
|
|
204
|
+
resolve(false);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
resolve(true); // "allow" or omitted → allow
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// Malformed JSON with a zero exit — fail closed conservatively.
|
|
212
|
+
resolve(false);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
proc.on("error", () => {
|
|
216
|
+
if (!settled) {
|
|
217
|
+
settled = true;
|
|
218
|
+
clearTimeout(timer);
|
|
219
|
+
resolve(false);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
}
|
|
144
224
|
/** Run an HTTP hook. POSTs context as JSON, expects { allowed: true/false }. */
|
|
145
225
|
async function runHttpHook(url, event, ctx, timeoutMs = 10_000) {
|
|
146
226
|
try {
|
|
@@ -161,15 +241,63 @@ async function runHttpHook(url, event, ctx, timeoutMs = 10_000) {
|
|
|
161
241
|
}
|
|
162
242
|
}
|
|
163
243
|
/**
|
|
164
|
-
* Run a prompt hook. Uses LLM to make a yes/no decision.
|
|
244
|
+
* Run a prompt hook. Uses an LLM to make a yes/no allow/deny decision.
|
|
245
|
+
*
|
|
246
|
+
* The hook's `prompt:` field is the question posed to the model along with
|
|
247
|
+
* the event context. The response is parsed case-insensitively: responses
|
|
248
|
+
* starting with YES / ALLOW / TRUE / PASS / APPROVE allow; anything else
|
|
249
|
+
* (including explicit NO, DENY, errors, timeouts, empty) blocks.
|
|
165
250
|
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
251
|
+
* Fail-closed semantics: if the provider isn't reachable or the response
|
|
252
|
+
* can't be parsed, the hook denies. This matches command hooks (non-zero
|
|
253
|
+
* exit = deny) and HTTP hooks (network error = deny).
|
|
254
|
+
*
|
|
255
|
+
* Provider selection: reads `.oh/config.yaml` to get the configured provider
|
|
256
|
+
* and model. A separate provider instance is created per call — no caching,
|
|
257
|
+
* since hooks are rare and cold-start cost is negligible compared to the
|
|
258
|
+
* LLM call itself.
|
|
170
259
|
*/
|
|
171
|
-
async function runPromptHook(
|
|
172
|
-
|
|
260
|
+
async function runPromptHook(promptText, ctx, timeoutMs = 10_000) {
|
|
261
|
+
try {
|
|
262
|
+
const cfg = readOhConfig();
|
|
263
|
+
if (!cfg)
|
|
264
|
+
return false; // no config → no provider → fail closed
|
|
265
|
+
const { createProvider } = (await import("../providers/index.js"));
|
|
266
|
+
const modelArg = cfg.model ? `${cfg.provider}/${cfg.model}` : cfg.provider;
|
|
267
|
+
const overrides = {};
|
|
268
|
+
if (cfg.apiKey)
|
|
269
|
+
overrides.apiKey = cfg.apiKey;
|
|
270
|
+
if (cfg.baseUrl)
|
|
271
|
+
overrides.baseUrl = cfg.baseUrl;
|
|
272
|
+
const { provider, model } = await createProvider(modelArg, overrides);
|
|
273
|
+
const systemPrompt = "You are a policy gate. Read the question and the event context. Answer with a single word: YES to allow, NO to deny. Do not explain unless asked.";
|
|
274
|
+
const userContent = [
|
|
275
|
+
`Question: ${promptText}`,
|
|
276
|
+
"",
|
|
277
|
+
"Event context:",
|
|
278
|
+
JSON.stringify({ event: ctx }, null, 2),
|
|
279
|
+
"",
|
|
280
|
+
"Answer (YES or NO):",
|
|
281
|
+
].join("\n");
|
|
282
|
+
const { createUserMessage } = (await import("../types/message.js"));
|
|
283
|
+
const messages = [createUserMessage(userContent)];
|
|
284
|
+
// Race the completion against a hard timeout so a hung provider doesn't
|
|
285
|
+
// block the agent loop indefinitely.
|
|
286
|
+
const completion = await Promise.race([
|
|
287
|
+
provider.complete(messages, systemPrompt, undefined, model),
|
|
288
|
+
new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs)),
|
|
289
|
+
]);
|
|
290
|
+
if (!completion)
|
|
291
|
+
return false; // timeout → deny
|
|
292
|
+
const text = (completion.content ?? "").trim().toUpperCase();
|
|
293
|
+
if (!text)
|
|
294
|
+
return false;
|
|
295
|
+
// Accept multiple allow synonyms; default to deny on anything else.
|
|
296
|
+
return /^(YES|ALLOW|TRUE|PASS|APPROVE)\b/.test(text);
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
return false; // any error path → deny
|
|
300
|
+
}
|
|
173
301
|
}
|
|
174
302
|
// ── Hook Execution ──
|
|
175
303
|
/** Execute a single hook definition. Returns true if allowed. */
|
|
@@ -177,6 +305,12 @@ async function executeHookDef(def, event, ctx) {
|
|
|
177
305
|
const timeout = def.timeout ?? 10_000;
|
|
178
306
|
if (def.command) {
|
|
179
307
|
const env = buildEnv(event, ctx);
|
|
308
|
+
// JSON-mode (Claude Code convention): send `{event, ...ctx}` on stdin,
|
|
309
|
+
// parse `{decision}` from stdout. Env-var mode (legacy default): gate on
|
|
310
|
+
// exit code.
|
|
311
|
+
if (def.jsonIO) {
|
|
312
|
+
return runJsonIoHookAsync(def.command, env, event, ctx, timeout);
|
|
313
|
+
}
|
|
180
314
|
const code = await runCommandHookAsync(def.command, env, timeout);
|
|
181
315
|
return code === 0;
|
|
182
316
|
}
|
|
@@ -206,14 +340,31 @@ export function emitHook(event, ctx = {}) {
|
|
|
206
340
|
if (!matchesHook(def, ctx))
|
|
207
341
|
continue;
|
|
208
342
|
if (def.command) {
|
|
343
|
+
const input = def.jsonIO ? JSON.stringify({ event, ...ctx }) : undefined;
|
|
209
344
|
const result = spawnSync(def.command, {
|
|
210
345
|
shell: true,
|
|
211
346
|
timeout: def.timeout ?? 10_000,
|
|
212
347
|
stdio: "pipe",
|
|
213
348
|
env,
|
|
349
|
+
input,
|
|
214
350
|
});
|
|
215
351
|
if (result.status !== 0 || result.error)
|
|
216
352
|
return false;
|
|
353
|
+
// JSON mode: parse stdout for {decision: "deny"} → block. Allow on empty
|
|
354
|
+
// stdout (exit-code already gated above). Malformed JSON fails closed.
|
|
355
|
+
if (def.jsonIO) {
|
|
356
|
+
const out = result.stdout?.toString() ?? "";
|
|
357
|
+
if (out.trim()) {
|
|
358
|
+
try {
|
|
359
|
+
const parsed = JSON.parse(out);
|
|
360
|
+
if (parsed.decision === "deny")
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
217
368
|
}
|
|
218
369
|
// HTTP and prompt hooks for preToolUse are handled in emitHookAsync
|
|
219
370
|
}
|
package/dist/mcp/client.d.ts
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
|
+
import type { Client as SdkClient } from "@modelcontextprotocol/sdk/client/index.js";
|
|
1
2
|
import type { McpServerConfig } from "../harness/config.js";
|
|
2
3
|
import type { McpToolDef } from "./types.js";
|
|
4
|
+
type ForTestingOptions = {
|
|
5
|
+
name: string;
|
|
6
|
+
cfg: McpServerConfig;
|
|
7
|
+
sdk: SdkClient;
|
|
8
|
+
timeoutMs: number;
|
|
9
|
+
reconnect?: () => Promise<SdkClient>;
|
|
10
|
+
};
|
|
3
11
|
export declare class McpClient {
|
|
4
12
|
readonly name: string;
|
|
5
|
-
|
|
6
|
-
private
|
|
7
|
-
private pending;
|
|
8
|
-
private ready;
|
|
9
|
-
private dead;
|
|
13
|
+
instructions: string | null;
|
|
14
|
+
private sdk;
|
|
10
15
|
private cfg;
|
|
11
16
|
private timeoutMs;
|
|
17
|
+
private reconnectImpl;
|
|
12
18
|
private constructor();
|
|
13
|
-
/** Server-provided instructions (from capabilities during init) */
|
|
14
|
-
instructions: string | null;
|
|
15
19
|
static connect(cfg: McpServerConfig, timeoutMs?: number): Promise<McpClient>;
|
|
20
|
+
/** Test-only constructor. Not exported from the package's public API. */
|
|
21
|
+
static _forTesting(opts: ForTestingOptions): McpClient;
|
|
22
|
+
private defaultReconnect;
|
|
16
23
|
listTools(): Promise<McpToolDef[]>;
|
|
17
24
|
listResources(): Promise<Array<{
|
|
18
25
|
uri: string;
|
|
@@ -21,8 +28,7 @@ export declare class McpClient {
|
|
|
21
28
|
}>>;
|
|
22
29
|
readResource(uri: string): Promise<string>;
|
|
23
30
|
callTool(name: string, args: Record<string, unknown>): Promise<string>;
|
|
24
|
-
private callWithTimeout;
|
|
25
|
-
private call;
|
|
26
31
|
disconnect(): void;
|
|
27
32
|
}
|
|
33
|
+
export {};
|
|
28
34
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/mcp/client.js
CHANGED
|
@@ -1,150 +1,97 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { normalizeMcpConfig } from "./config-normalize.js";
|
|
2
|
+
import { buildClient, connectWithFallback } from "./transport.js";
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
4
4
|
export class McpClient {
|
|
5
5
|
name;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
pending = new Map();
|
|
9
|
-
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: set via Object.assign in static factory
|
|
10
|
-
ready = false;
|
|
11
|
-
dead = false;
|
|
6
|
+
instructions = null;
|
|
7
|
+
sdk;
|
|
12
8
|
cfg;
|
|
13
9
|
timeoutMs;
|
|
14
|
-
|
|
10
|
+
reconnectImpl;
|
|
11
|
+
constructor(name, cfg, sdk, timeoutMs, reconnect) {
|
|
15
12
|
this.name = name;
|
|
16
|
-
this.proc = proc;
|
|
17
13
|
this.cfg = cfg;
|
|
14
|
+
this.sdk = sdk;
|
|
18
15
|
this.timeoutMs = timeoutMs;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (p) {
|
|
25
|
-
this.pending.delete(msg.id);
|
|
26
|
-
p.resolve(msg);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
// non-JSON line from server (e.g. startup noise) — ignore
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
proc.on("exit", () => {
|
|
34
|
-
this.dead = true;
|
|
35
|
-
for (const p of this.pending.values()) {
|
|
36
|
-
p.reject(new Error(`MCP server '${name}' exited`));
|
|
37
|
-
}
|
|
38
|
-
this.pending.clear();
|
|
39
|
-
});
|
|
16
|
+
this.reconnectImpl = reconnect ?? (() => this.defaultReconnect());
|
|
17
|
+
const instr = sdk.getInstructions?.();
|
|
18
|
+
if (instr && typeof instr === "string") {
|
|
19
|
+
this.instructions = instr;
|
|
20
|
+
}
|
|
40
21
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
46
|
-
env: safeEnv(cfg.env),
|
|
47
|
-
});
|
|
48
|
-
const client = new McpClient(cfg.name, proc, cfg, timeoutMs);
|
|
49
|
-
// Initialize handshake
|
|
50
|
-
const initResponse = await Promise.race([
|
|
51
|
-
client.call("initialize", {
|
|
52
|
-
protocolVersion: "2024-11-05",
|
|
53
|
-
clientInfo: { name: "openharness", version: "0.2.1" },
|
|
54
|
-
capabilities: {},
|
|
55
|
-
}),
|
|
56
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error(`MCP '${cfg.name}' init timeout`)), timeoutMs)),
|
|
57
|
-
]);
|
|
58
|
-
// Extract server instructions from init response
|
|
59
|
-
const serverInfo = initResponse?.result;
|
|
60
|
-
if (serverInfo?.instructions && typeof serverInfo.instructions === "string") {
|
|
61
|
-
client.instructions = serverInfo.instructions;
|
|
22
|
+
static async connect(cfg, timeoutMs = cfg.timeout ?? DEFAULT_TIMEOUT_MS) {
|
|
23
|
+
const normalized = normalizeMcpConfig(cfg, process.env);
|
|
24
|
+
if (normalized.kind === "error") {
|
|
25
|
+
throw new Error(normalized.message);
|
|
62
26
|
}
|
|
63
|
-
await
|
|
64
|
-
|
|
65
|
-
|
|
27
|
+
const sdk = await connectWithFallback(normalized.cfg, (c) => buildClient(c));
|
|
28
|
+
return new McpClient(cfg.name, cfg, sdk, timeoutMs);
|
|
29
|
+
}
|
|
30
|
+
/** Test-only constructor. Not exported from the package's public API. */
|
|
31
|
+
static _forTesting(opts) {
|
|
32
|
+
return new McpClient(opts.name, opts.cfg, opts.sdk, opts.timeoutMs, opts.reconnect);
|
|
33
|
+
}
|
|
34
|
+
async defaultReconnect() {
|
|
35
|
+
const normalized = normalizeMcpConfig(this.cfg, process.env);
|
|
36
|
+
if (normalized.kind === "error")
|
|
37
|
+
throw new Error(normalized.message);
|
|
38
|
+
return connectWithFallback(normalized.cfg, (c) => buildClient(c));
|
|
66
39
|
}
|
|
67
40
|
async listTools() {
|
|
68
|
-
const res = await this.
|
|
69
|
-
return (res
|
|
41
|
+
const res = await this.sdk.listTools();
|
|
42
|
+
return (res?.tools ?? []);
|
|
70
43
|
}
|
|
71
44
|
async listResources() {
|
|
72
45
|
try {
|
|
73
|
-
const res = await this.
|
|
74
|
-
return (res
|
|
46
|
+
const res = await this.sdk.listResources();
|
|
47
|
+
return (res?.resources ?? []);
|
|
75
48
|
}
|
|
76
49
|
catch {
|
|
77
50
|
return []; // Server may not support resources
|
|
78
51
|
}
|
|
79
52
|
}
|
|
80
53
|
async readResource(uri) {
|
|
81
|
-
const res = await this.
|
|
82
|
-
|
|
83
|
-
throw new Error(res.error.message);
|
|
84
|
-
const contents = res.result?.contents ?? [];
|
|
54
|
+
const res = await this.sdk.readResource({ uri });
|
|
55
|
+
const contents = res?.contents ?? [];
|
|
85
56
|
return contents
|
|
86
|
-
.filter((c) => c.text)
|
|
57
|
+
.filter((c) => typeof c.text === "string")
|
|
87
58
|
.map((c) => c.text)
|
|
88
59
|
.join("\n");
|
|
89
60
|
}
|
|
90
61
|
async callTool(name, args) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const fresh = await McpClient.connect(this.cfg, this.timeoutMs);
|
|
94
|
-
Object.assign(this, { proc: fresh.proc, dead: false, ready: true, nextId: 1, pending: new Map() });
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
throw new Error(`MCP server '${this.name}' died and restart failed`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
// Retry up to 2 times for transient failures
|
|
101
|
-
let lastError = null;
|
|
62
|
+
// Retry up to 2 times on transport-closed / timeout errors
|
|
63
|
+
let lastErr = null;
|
|
102
64
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
103
65
|
try {
|
|
104
|
-
const res = await this.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return content
|
|
109
|
-
.filter((c) => c.type === "text")
|
|
66
|
+
const res = await this.sdk.callTool({ name, arguments: args });
|
|
67
|
+
const content = (res?.content ?? []);
|
|
68
|
+
const text = content
|
|
69
|
+
.filter((c) => c.type === "text" && typeof c.text === "string")
|
|
110
70
|
.map((c) => c.text)
|
|
111
71
|
.join("\n");
|
|
72
|
+
if (res?.isError) {
|
|
73
|
+
throw new Error(text || `MCP tool '${name}' returned an error`);
|
|
74
|
+
}
|
|
75
|
+
return text;
|
|
112
76
|
}
|
|
113
77
|
catch (err) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
78
|
+
lastErr = err instanceof Error ? err : new Error(String(err));
|
|
79
|
+
const msg = lastErr.message;
|
|
80
|
+
const retryable = /transport closed|timeout|ECONNRESET|stream closed|socket hang up/i.test(msg);
|
|
81
|
+
if (!retryable || attempt === 2)
|
|
82
|
+
throw lastErr;
|
|
83
|
+
try {
|
|
84
|
+
this.sdk = await this.reconnectImpl();
|
|
118
85
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const fresh = await McpClient.connect(this.cfg, this.timeoutMs);
|
|
122
|
-
Object.assign(this, { proc: fresh.proc, dead: false, ready: true, nextId: 1, pending: new Map() });
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
throw new Error(`MCP server '${this.name}' died and restart failed`);
|
|
126
|
-
}
|
|
86
|
+
catch (reErr) {
|
|
87
|
+
throw new Error(`MCP '${this.name}' died and reconnect failed: ${reErr instanceof Error ? reErr.message : String(reErr)}`);
|
|
127
88
|
}
|
|
128
89
|
}
|
|
129
90
|
}
|
|
130
|
-
throw
|
|
131
|
-
}
|
|
132
|
-
callWithTimeout(method, params) {
|
|
133
|
-
return Promise.race([
|
|
134
|
-
this.call(method, params),
|
|
135
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error(`MCP '${this.name}' call timeout (${this.timeoutMs}ms)`)), this.timeoutMs)),
|
|
136
|
-
]);
|
|
137
|
-
}
|
|
138
|
-
call(method, params) {
|
|
139
|
-
return new Promise((resolve, reject) => {
|
|
140
|
-
const id = this.nextId++;
|
|
141
|
-
const req = { jsonrpc: "2.0", id, method, params };
|
|
142
|
-
this.pending.set(id, { resolve, reject });
|
|
143
|
-
this.proc.stdin.write(`${JSON.stringify(req)}\n`);
|
|
144
|
-
});
|
|
91
|
+
throw lastErr ?? new Error(`MCP '${this.name}' callTool failed after retries`);
|
|
145
92
|
}
|
|
146
93
|
disconnect() {
|
|
147
|
-
this.
|
|
94
|
+
void this.sdk.close?.();
|
|
148
95
|
}
|
|
149
96
|
}
|
|
150
97
|
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { McpHttpConfig, McpServerConfig, McpSseConfig, McpStdioConfig } from "../harness/config.js";
|
|
2
|
+
/** Discriminated-union result: either a validated config or a human-readable error. */
|
|
3
|
+
export type NormalizeResult = {
|
|
4
|
+
kind: "ok";
|
|
5
|
+
cfg: NormalizedConfig;
|
|
6
|
+
} | {
|
|
7
|
+
kind: "error";
|
|
8
|
+
message: string;
|
|
9
|
+
};
|
|
10
|
+
export type NormalizedConfig = (McpStdioConfig & {
|
|
11
|
+
type: "stdio";
|
|
12
|
+
}) | (McpHttpConfig & {
|
|
13
|
+
inferredFromUrl?: boolean;
|
|
14
|
+
}) | (McpSseConfig & {
|
|
15
|
+
inferredFromUrl?: boolean;
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* Validate + normalize a raw MCP server config entry.
|
|
19
|
+
* - Infers missing `type` from `command`/`url`.
|
|
20
|
+
* - Interpolates ${ENV} in headers (http/sse only).
|
|
21
|
+
* - Returns {kind:"error"} with a reason for any invalid combination.
|
|
22
|
+
*/
|
|
23
|
+
export declare function normalizeMcpConfig(raw: McpServerConfig, env: Record<string, string | undefined>): NormalizeResult;
|
|
24
|
+
//# sourceMappingURL=config-normalize.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const ENV_REF = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
|
|
2
|
+
/** Replace ${VAR} references in `value` from `env`. Returns the new string or a missing-var name. */
|
|
3
|
+
function interpolate(value, env) {
|
|
4
|
+
let missing = null;
|
|
5
|
+
const out = value.replace(ENV_REF, (_match, varName) => {
|
|
6
|
+
const v = env[varName];
|
|
7
|
+
if (v === undefined) {
|
|
8
|
+
if (missing === null)
|
|
9
|
+
missing = varName;
|
|
10
|
+
return "";
|
|
11
|
+
}
|
|
12
|
+
return v;
|
|
13
|
+
});
|
|
14
|
+
if (missing !== null)
|
|
15
|
+
return { ok: false, missing };
|
|
16
|
+
return { ok: true, value: out };
|
|
17
|
+
}
|
|
18
|
+
function interpolateHeaders(headers, env) {
|
|
19
|
+
if (!headers)
|
|
20
|
+
return { ok: true, headers: undefined };
|
|
21
|
+
const out = {};
|
|
22
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
23
|
+
const r = interpolate(v, env);
|
|
24
|
+
if (!r.ok)
|
|
25
|
+
return { ok: false, missing: r.missing };
|
|
26
|
+
out[k] = r.value;
|
|
27
|
+
}
|
|
28
|
+
return { ok: true, headers: out };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Validate + normalize a raw MCP server config entry.
|
|
32
|
+
* - Infers missing `type` from `command`/`url`.
|
|
33
|
+
* - Interpolates ${ENV} in headers (http/sse only).
|
|
34
|
+
* - Returns {kind:"error"} with a reason for any invalid combination.
|
|
35
|
+
*/
|
|
36
|
+
export function normalizeMcpConfig(raw, env) {
|
|
37
|
+
const hasCommand = "command" in raw && !!raw.command;
|
|
38
|
+
const hasUrl = "url" in raw && !!raw.url;
|
|
39
|
+
if (hasCommand && hasUrl) {
|
|
40
|
+
return { kind: "error", message: `MCP '${raw.name}': config sets both 'command' and 'url'` };
|
|
41
|
+
}
|
|
42
|
+
const declaredType = raw.type;
|
|
43
|
+
const effectiveType = declaredType ?? (hasCommand ? "stdio" : hasUrl ? "http" : undefined);
|
|
44
|
+
if (!effectiveType) {
|
|
45
|
+
return { kind: "error", message: `MCP '${raw.name}': must set 'command' (stdio) or 'url' (http/sse)` };
|
|
46
|
+
}
|
|
47
|
+
if (effectiveType === "stdio") {
|
|
48
|
+
if (!hasCommand) {
|
|
49
|
+
return { kind: "error", message: `MCP '${raw.name}': type='stdio' requires 'command'` };
|
|
50
|
+
}
|
|
51
|
+
return { kind: "ok", cfg: { ...raw, type: "stdio" } };
|
|
52
|
+
}
|
|
53
|
+
// http or sse
|
|
54
|
+
if (!hasUrl) {
|
|
55
|
+
return { kind: "error", message: `MCP '${raw.name}': type='${effectiveType}' requires 'url'` };
|
|
56
|
+
}
|
|
57
|
+
const headers = raw.headers;
|
|
58
|
+
const interp = interpolateHeaders(headers, env);
|
|
59
|
+
if (!interp.ok) {
|
|
60
|
+
return {
|
|
61
|
+
kind: "error",
|
|
62
|
+
message: `MCP '${raw.name}': env var '${interp.missing}' referenced in headers is not set`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const inferred = declaredType === undefined;
|
|
66
|
+
const base = { ...raw, type: effectiveType, headers: interp.headers };
|
|
67
|
+
return {
|
|
68
|
+
kind: "ok",
|
|
69
|
+
cfg: inferred ? { ...base, inferredFromUrl: true } : base,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=config-normalize.js.map
|
package/dist/mcp/loader.js
CHANGED
|
@@ -3,10 +3,34 @@ import { McpClient } from "./client.js";
|
|
|
3
3
|
import { DeferredMcpTool } from "./DeferredMcpTool.js";
|
|
4
4
|
import { McpTool } from "./McpTool.js";
|
|
5
5
|
const connectedClients = [];
|
|
6
|
+
let exitHandlerInstalled = false;
|
|
7
|
+
function installExitHandler() {
|
|
8
|
+
if (exitHandlerInstalled)
|
|
9
|
+
return;
|
|
10
|
+
exitHandlerInstalled = true;
|
|
11
|
+
const handler = () => {
|
|
12
|
+
try {
|
|
13
|
+
disconnectMcpClients();
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
/* shutdown best-effort */
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
process.once("exit", handler);
|
|
20
|
+
process.once("SIGINT", () => {
|
|
21
|
+
handler();
|
|
22
|
+
process.exit(130);
|
|
23
|
+
});
|
|
24
|
+
process.once("SIGTERM", () => {
|
|
25
|
+
handler();
|
|
26
|
+
process.exit(143);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
6
29
|
/** Threshold: servers with more tools than this use deferred loading */
|
|
7
30
|
const DEFERRED_THRESHOLD = 10;
|
|
8
31
|
/** Load MCP tools from .oh/config.yaml mcpServers list. Returns empty array if none configured. */
|
|
9
32
|
export async function loadMcpTools() {
|
|
33
|
+
installExitHandler();
|
|
10
34
|
const cfg = readOhConfig();
|
|
11
35
|
const servers = cfg?.mcpServers ?? [];
|
|
12
36
|
if (servers.length === 0)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
3
|
+
import type { NormalizedConfig } from "./config-normalize.js";
|
|
4
|
+
export declare class RemoteAuthRequiredError extends Error {
|
|
5
|
+
readonly serverName: string;
|
|
6
|
+
readonly wwwAuthenticate: string | undefined;
|
|
7
|
+
constructor(serverName: string, wwwAuthenticate: string | undefined);
|
|
8
|
+
}
|
|
9
|
+
export declare class UnreachableError extends Error {
|
|
10
|
+
readonly serverName: string;
|
|
11
|
+
readonly cause: unknown;
|
|
12
|
+
constructor(serverName: string, cause: unknown);
|
|
13
|
+
}
|
|
14
|
+
export declare class ProtocolError extends Error {
|
|
15
|
+
readonly serverName: string;
|
|
16
|
+
readonly cause: unknown;
|
|
17
|
+
constructor(serverName: string, cause: unknown);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Construct an SDK Transport for a normalized config.
|
|
21
|
+
* Does NOT call .start() — caller (Client.connect) handles that.
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildTransport(cfg: NormalizedConfig): Promise<Transport>;
|
|
24
|
+
/**
|
|
25
|
+
* Connect to an MCP server, with auto-fallback from Streamable HTTP to
|
|
26
|
+
* legacy SSE when the config's type was INFERRED from url (not explicit).
|
|
27
|
+
*
|
|
28
|
+
* `doConnect` is the side-effecting step — build/connect a transport and
|
|
29
|
+
* return the opaque client. Kept injectable for tests; production call-site
|
|
30
|
+
* wires it to `buildClient` (Task 7).
|
|
31
|
+
*/
|
|
32
|
+
export declare function connectWithFallback<T>(cfg: NormalizedConfig, doConnect: (cfg: NormalizedConfig) => Promise<T>): Promise<T>;
|
|
33
|
+
/**
|
|
34
|
+
* Build a connected SDK Client for a normalized config.
|
|
35
|
+
* Maps connect-time errors into OH's typed error taxonomy.
|
|
36
|
+
*/
|
|
37
|
+
export declare function buildClient(cfg: NormalizedConfig): Promise<Client>;
|
|
38
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
4
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
5
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
6
|
+
const pkg = createRequire(import.meta.url)("../../package.json");
|
|
7
|
+
export class RemoteAuthRequiredError extends Error {
|
|
8
|
+
serverName;
|
|
9
|
+
wwwAuthenticate;
|
|
10
|
+
constructor(serverName, wwwAuthenticate) {
|
|
11
|
+
super(`MCP server '${serverName}' requires authentication. ` +
|
|
12
|
+
`Add headers.Authorization to your config (OAuth flow is not yet supported).`);
|
|
13
|
+
this.name = "RemoteAuthRequiredError";
|
|
14
|
+
this.serverName = serverName;
|
|
15
|
+
this.wwwAuthenticate = wwwAuthenticate;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class UnreachableError extends Error {
|
|
19
|
+
serverName;
|
|
20
|
+
cause;
|
|
21
|
+
constructor(serverName, cause) {
|
|
22
|
+
const causeMsg = cause instanceof Error ? cause.message : String(cause);
|
|
23
|
+
super(`MCP server '${serverName}' unreachable: ${causeMsg}`);
|
|
24
|
+
this.name = "UnreachableError";
|
|
25
|
+
this.serverName = serverName;
|
|
26
|
+
this.cause = cause;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export class ProtocolError extends Error {
|
|
30
|
+
serverName;
|
|
31
|
+
cause;
|
|
32
|
+
constructor(serverName, cause) {
|
|
33
|
+
const causeMsg = cause instanceof Error ? cause.message : String(cause);
|
|
34
|
+
super(`MCP server '${serverName}' protocol error: ${causeMsg}`);
|
|
35
|
+
this.name = "ProtocolError";
|
|
36
|
+
this.serverName = serverName;
|
|
37
|
+
this.cause = cause;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Construct an SDK Transport for a normalized config.
|
|
42
|
+
* Does NOT call .start() — caller (Client.connect) handles that.
|
|
43
|
+
*/
|
|
44
|
+
export async function buildTransport(cfg) {
|
|
45
|
+
if (cfg.type === "stdio") {
|
|
46
|
+
return new StdioClientTransport({
|
|
47
|
+
command: cfg.command,
|
|
48
|
+
args: cfg.args,
|
|
49
|
+
env: cfg.env,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (cfg.type === "http") {
|
|
53
|
+
return new StreamableHTTPClientTransport(new URL(cfg.url), {
|
|
54
|
+
requestInit: cfg.headers ? { headers: cfg.headers } : undefined,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (cfg.type === "sse") {
|
|
58
|
+
return new SSEClientTransport(new URL(cfg.url), {
|
|
59
|
+
requestInit: cfg.headers ? { headers: cfg.headers } : undefined,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`unknown transport type: ${cfg.type}`);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Does this error indicate "try the legacy SSE transport instead"?
|
|
66
|
+
* Yes for 4xx OTHER than 401-with-WWW-Authenticate (which is a real auth challenge).
|
|
67
|
+
*/
|
|
68
|
+
function isFallbackCandidate(err) {
|
|
69
|
+
const code = err?.code;
|
|
70
|
+
if (typeof code !== "number")
|
|
71
|
+
return false;
|
|
72
|
+
if (code < 400 || code >= 500)
|
|
73
|
+
return false;
|
|
74
|
+
if (code === 401) {
|
|
75
|
+
const www = err?.headers?.["www-authenticate"];
|
|
76
|
+
if (typeof www === "string" && www.length > 0)
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
function extractWwwAuthenticate(err) {
|
|
82
|
+
const code = err?.code;
|
|
83
|
+
if (code !== 401)
|
|
84
|
+
return undefined;
|
|
85
|
+
const www = err?.headers?.["www-authenticate"];
|
|
86
|
+
return typeof www === "string" ? www : undefined;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Connect to an MCP server, with auto-fallback from Streamable HTTP to
|
|
90
|
+
* legacy SSE when the config's type was INFERRED from url (not explicit).
|
|
91
|
+
*
|
|
92
|
+
* `doConnect` is the side-effecting step — build/connect a transport and
|
|
93
|
+
* return the opaque client. Kept injectable for tests; production call-site
|
|
94
|
+
* wires it to `buildClient` (Task 7).
|
|
95
|
+
*/
|
|
96
|
+
export async function connectWithFallback(cfg, doConnect) {
|
|
97
|
+
try {
|
|
98
|
+
return await doConnect(cfg);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
// Auth challenge → surface immediately, never fall back
|
|
102
|
+
const www = extractWwwAuthenticate(err);
|
|
103
|
+
if (www !== undefined)
|
|
104
|
+
throw new RemoteAuthRequiredError(cfg.name, www);
|
|
105
|
+
// Explicit type → surface as-is
|
|
106
|
+
const inferred = cfg.inferredFromUrl === true;
|
|
107
|
+
if (!inferred)
|
|
108
|
+
throw err;
|
|
109
|
+
// Only http-was-inferred-first falls back to sse; other shapes surface
|
|
110
|
+
if (cfg.type !== "http")
|
|
111
|
+
throw err;
|
|
112
|
+
if (!isFallbackCandidate(err))
|
|
113
|
+
throw err;
|
|
114
|
+
// Log + retry
|
|
115
|
+
// biome-ignore lint/suspicious/noConsole: user-facing diagnostic
|
|
116
|
+
console.warn(`[mcp] ${cfg.name}: Streamable HTTP failed (${err.message}); trying legacy SSE`);
|
|
117
|
+
const sseCfg = { ...cfg, type: "sse" };
|
|
118
|
+
return await doConnect(sseCfg);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
122
|
+
const CLIENT_INFO = { name: "openharness", version: pkg.version };
|
|
123
|
+
/**
|
|
124
|
+
* Build a connected SDK Client for a normalized config.
|
|
125
|
+
* Maps connect-time errors into OH's typed error taxonomy.
|
|
126
|
+
*/
|
|
127
|
+
export async function buildClient(cfg) {
|
|
128
|
+
const transport = await buildTransport(cfg);
|
|
129
|
+
const client = new Client(CLIENT_INFO, { capabilities: {} });
|
|
130
|
+
const timeoutMs = cfg.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
131
|
+
let timer = null;
|
|
132
|
+
try {
|
|
133
|
+
await Promise.race([
|
|
134
|
+
client.connect(transport),
|
|
135
|
+
new Promise((_, reject) => {
|
|
136
|
+
timer = setTimeout(() => reject(new Error(`init timeout after ${timeoutMs}ms`)), timeoutMs);
|
|
137
|
+
}),
|
|
138
|
+
]);
|
|
139
|
+
return client;
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
// Leave RemoteAuthRequiredError / UnreachableError / ProtocolError as-is
|
|
143
|
+
if (err instanceof RemoteAuthRequiredError || err instanceof UnreachableError || err instanceof ProtocolError) {
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
// Network-shaped errors (DNS, TCP, TLS, timeout) → Unreachable
|
|
147
|
+
const msg = err?.message ?? String(err);
|
|
148
|
+
if (/timeout|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|EAI_AGAIN|network|fetch failed/i.test(msg)) {
|
|
149
|
+
throw new UnreachableError(cfg.name, err);
|
|
150
|
+
}
|
|
151
|
+
// Otherwise protocol-shaped
|
|
152
|
+
throw new ProtocolError(cfg.name, err);
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
if (timer !== null)
|
|
156
|
+
clearTimeout(timer);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=transport.js.map
|
package/dist/mcp/types.d.ts
CHANGED
|
@@ -1,19 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export interface JsonRpcRequest {
|
|
3
|
-
jsonrpc: "2.0";
|
|
4
|
-
id: number;
|
|
5
|
-
method: string;
|
|
6
|
-
params?: unknown;
|
|
7
|
-
}
|
|
8
|
-
export interface JsonRpcResponse {
|
|
9
|
-
jsonrpc: "2.0";
|
|
10
|
-
id: number;
|
|
11
|
-
result?: unknown;
|
|
12
|
-
error?: {
|
|
13
|
-
code: number;
|
|
14
|
-
message: string;
|
|
15
|
-
};
|
|
16
|
-
}
|
|
1
|
+
/** MCP tool definition as returned by `tools/list`. */
|
|
17
2
|
export interface McpToolDef {
|
|
18
3
|
name: string;
|
|
19
4
|
description?: string;
|
package/dist/mcp/types.js
CHANGED
|
@@ -12,16 +12,16 @@ declare const inputSchema: z.ZodObject<{
|
|
|
12
12
|
action: "search" | "save" | "list";
|
|
13
13
|
content?: string | undefined;
|
|
14
14
|
type?: "user" | "convention" | "preference" | "project" | "debugging" | "feedback" | "reference" | undefined;
|
|
15
|
-
description?: string | undefined;
|
|
16
15
|
name?: string | undefined;
|
|
16
|
+
description?: string | undefined;
|
|
17
17
|
global?: boolean | undefined;
|
|
18
18
|
query?: string | undefined;
|
|
19
19
|
}, {
|
|
20
20
|
action: "search" | "save" | "list";
|
|
21
21
|
content?: string | undefined;
|
|
22
22
|
type?: "user" | "convention" | "preference" | "project" | "debugging" | "feedback" | "reference" | undefined;
|
|
23
|
-
description?: string | undefined;
|
|
24
23
|
name?: string | undefined;
|
|
24
|
+
description?: string | undefined;
|
|
25
25
|
global?: boolean | undefined;
|
|
26
26
|
query?: string | undefined;
|
|
27
27
|
}>;
|
|
@@ -12,7 +12,7 @@ declare const inputSchema: z.ZodObject<{
|
|
|
12
12
|
addBlockedBy: z.ZodOptional<z.ZodArray<z.ZodNumber, "many">>;
|
|
13
13
|
}, "strip", z.ZodTypeAny, {
|
|
14
14
|
taskId: number;
|
|
15
|
-
status?: "completed" | "
|
|
15
|
+
status?: "completed" | "cancelled" | "pending" | "in_progress" | "deleted" | undefined;
|
|
16
16
|
description?: string | undefined;
|
|
17
17
|
metadata?: Record<string, unknown> | undefined;
|
|
18
18
|
subject?: string | undefined;
|
|
@@ -22,7 +22,7 @@ declare const inputSchema: z.ZodObject<{
|
|
|
22
22
|
addBlockedBy?: number[] | undefined;
|
|
23
23
|
}, {
|
|
24
24
|
taskId: number;
|
|
25
|
-
status?: "completed" | "
|
|
25
|
+
status?: "completed" | "cancelled" | "pending" | "in_progress" | "deleted" | undefined;
|
|
26
26
|
description?: string | undefined;
|
|
27
27
|
metadata?: Record<string, unknown> | undefined;
|
|
28
28
|
subject?: string | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhijiewang/openharness",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
4
4
|
"description": "Open-source terminal coding agent. Works with any LLM.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"start": "node dist/main.js"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
39
40
|
"better-sqlite3": "^12.9.0",
|
|
40
41
|
"chalk": "^5.4.1",
|
|
41
42
|
"commander": "^13.0.0",
|