@houndmcp/hound-mcp-pi 10.3.0 → 10.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/README.md +19 -1
- package/extensions/hound.ts +143 -49
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -15,12 +15,30 @@ Hound web research for the Pi agent. Six native tools, all routed through a sing
|
|
|
15
15
|
# 1. Install Hound (the MCP server / engine)
|
|
16
16
|
pip install hound-mcp[all]
|
|
17
17
|
|
|
18
|
-
# 2. Install this Pi extension
|
|
18
|
+
# 2. Install this Pi extension (npm, auto-updates)
|
|
19
|
+
pi install npm:@houndmcp/hound-mcp-pi
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Alternatively, install from git (pins to a specific tag):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
19
25
|
pi install git:github.com/dondai1234/master-fetch@v10.3.0
|
|
20
26
|
```
|
|
21
27
|
|
|
22
28
|
That's it. The extension auto-discovers `hound` on your PATH, spawns it as a singleton subprocess, and prewarms it at session start. No API key, no account, no config file.
|
|
23
29
|
|
|
30
|
+
## Updating
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Update Hound (the MCP server)
|
|
34
|
+
hound -u
|
|
35
|
+
|
|
36
|
+
# Update the Pi extension (if installed via npm, unpinned)
|
|
37
|
+
pi update npm:@houndmcp/hound-mcp-pi
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The extension checks at session start whether the installed Hound version matches the extension version. If they diverge by a major version, it warns you to update both.
|
|
41
|
+
|
|
24
42
|
## How it works
|
|
25
43
|
|
|
26
44
|
The extension spawns `hound` (the MCP server) as a stdio subprocess and speaks MCP JSON-RPC to it. The subprocess is a singleton — it stays alive for the whole Pi session, so Hound's startup prewarm (stealthy browser, search engines, neural reranker) runs once and persists. Zero re-launch cost per call.
|
package/extensions/hound.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* resolved from PATH so it tracks the installed Hound version automatically.
|
|
16
16
|
*
|
|
17
17
|
* Install: pip install hound-mcp[all]
|
|
18
|
-
* Then: pi install
|
|
18
|
+
* Then: pi install npm:@houndmcp/hound-mcp-pi
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { Type } from "typebox";
|
|
@@ -26,9 +26,24 @@ import * as path from "node:path";
|
|
|
26
26
|
import * as os from "node:os";
|
|
27
27
|
import * as fs from "node:fs";
|
|
28
28
|
|
|
29
|
-
// --
|
|
29
|
+
// -- Version (read from package.json at load time) --
|
|
30
|
+
|
|
31
|
+
function readPkgVersion(): string {
|
|
32
|
+
try {
|
|
33
|
+
const pkgPath = path.join(__dirname, "..", "package.json");
|
|
34
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
35
|
+
return pkg.version || "unknown";
|
|
36
|
+
} catch {
|
|
37
|
+
return "unknown";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const EXTENSION_VERSION = readPkgVersion();
|
|
42
|
+
|
|
43
|
+
// -- Hound executable resolution (lazy, re-tried on each ensureReady if null) --
|
|
30
44
|
|
|
31
45
|
function resolveHoundExe(): string | null {
|
|
46
|
+
// 1. PATH lookup (covers 99% of installs)
|
|
32
47
|
try {
|
|
33
48
|
const cmd = process.platform === "win32" ? "where hound.exe" : "which hound";
|
|
34
49
|
const out = execSync(cmd, {
|
|
@@ -39,14 +54,17 @@ function resolveHoundExe(): string | null {
|
|
|
39
54
|
const first = out.split(/\r?\n/)[0];
|
|
40
55
|
if (first && fs.existsSync(first)) return first;
|
|
41
56
|
} catch {}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
// 2. Fallback: common Windows install paths (multiple Python versions)
|
|
58
|
+
if (process.platform === "win32") {
|
|
59
|
+
const home = os.homedir();
|
|
60
|
+
for (const v of ["Python311", "Python312", "Python313", "Python310"]) {
|
|
61
|
+
const p = path.join(home, "AppData", "Local", "Programs", "Python", v, "Scripts", "hound.exe");
|
|
62
|
+
if (fs.existsSync(p)) return p;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
47
66
|
}
|
|
48
67
|
|
|
49
|
-
const HOUND_EXE = resolveHoundExe();
|
|
50
68
|
const INIT_TIMEOUT_MS = 15_000;
|
|
51
69
|
const CALL_TIMEOUT_MS = 90_000;
|
|
52
70
|
const CRAWL_TIMEOUT_MS = 150_000;
|
|
@@ -54,20 +72,6 @@ const INIT_ATTEMPTS = 3;
|
|
|
54
72
|
const INIT_BACKOFF_MS = 400;
|
|
55
73
|
|
|
56
74
|
// -- MCP Stdio Client (singleton subprocess + JSON-RPC) --
|
|
57
|
-
//
|
|
58
|
-
// Hound is spawned once per Pi session and kept alive. Its startup prewarm
|
|
59
|
-
// (stealthy browser + search-engine sessions + neural reranker ONNX load)
|
|
60
|
-
// runs in the background during session_start, so by the time the agent
|
|
61
|
-
// makes its first web_fetch/search call, Hound is already warm. A dead
|
|
62
|
-
// subprocess (crash, OS kill) triggers a transparent re-init on the next
|
|
63
|
-
// call - callers never see a raw spawn error, just a slightly slower call.
|
|
64
|
-
//
|
|
65
|
-
// The MCP initialize handshake has a hard timeout. Hound occasionally spawns
|
|
66
|
-
// dead: its synchronous import playwright (via _get_scrapling) runs on the
|
|
67
|
-
// single-threaded asyncio loop and freezes it so server.run can never write
|
|
68
|
-
// the reply. The process is alive but mute - no stderr, no exit - so a longer
|
|
69
|
-
// timeout cannot help. A fresh re-spawn reliably responds in ~2-3s, so we
|
|
70
|
-
// kill the dead one and retry up to INIT_ATTEMPTS times.
|
|
71
75
|
|
|
72
76
|
interface Pending {
|
|
73
77
|
resolve: (v: any) => void;
|
|
@@ -77,6 +81,8 @@ interface Pending {
|
|
|
77
81
|
|
|
78
82
|
class HoundClient {
|
|
79
83
|
private proc: ChildProcess | null = null;
|
|
84
|
+
private houndExe: string | null = null;
|
|
85
|
+
private houndVersion: string | null = null;
|
|
80
86
|
private ready: Promise<boolean> | null = null;
|
|
81
87
|
private initInFlight: Promise<boolean> | null = null;
|
|
82
88
|
private nextId = 0;
|
|
@@ -84,9 +90,17 @@ class HoundClient {
|
|
|
84
90
|
private stdoutBuf = "";
|
|
85
91
|
private lastStderr = "";
|
|
86
92
|
|
|
93
|
+
getExe(): string | null { return this.houndExe; }
|
|
94
|
+
|
|
95
|
+
hasExe(): boolean { return this.houndExe !== null; }
|
|
96
|
+
|
|
87
97
|
async ensureReady(): Promise<boolean> {
|
|
88
98
|
if (this.ready) return this.ready;
|
|
89
|
-
if (
|
|
99
|
+
// Re-resolve hound exe if not found yet (handles install-after-load)
|
|
100
|
+
if (!this.houndExe) {
|
|
101
|
+
this.houndExe = resolveHoundExe();
|
|
102
|
+
}
|
|
103
|
+
if (!this.houndExe) return false;
|
|
90
104
|
if (this.initInFlight) return this.initInFlight;
|
|
91
105
|
const p = this._initWithRetry();
|
|
92
106
|
this.initInFlight = p;
|
|
@@ -107,6 +121,13 @@ class HoundClient {
|
|
|
107
121
|
}
|
|
108
122
|
|
|
109
123
|
private _initOnce(): Promise<boolean> {
|
|
124
|
+
// Kill any existing process before spawning a new one (prevents orphans)
|
|
125
|
+
if (this.proc && !this.proc.killed) {
|
|
126
|
+
try { this.proc.stdin?.end(); } catch {}
|
|
127
|
+
try { this.proc.kill(); } catch {}
|
|
128
|
+
}
|
|
129
|
+
this.proc = null;
|
|
130
|
+
|
|
110
131
|
return new Promise<boolean>((resolve) => {
|
|
111
132
|
let settled = false;
|
|
112
133
|
let initTimer: NodeJS.Timeout | undefined;
|
|
@@ -115,11 +136,11 @@ class HoundClient {
|
|
|
115
136
|
if (settled) return;
|
|
116
137
|
settled = true;
|
|
117
138
|
if (initTimer) clearTimeout(initTimer);
|
|
118
|
-
if (!ok) this.
|
|
139
|
+
if (!ok) this._killProcess();
|
|
119
140
|
resolve(ok);
|
|
120
141
|
};
|
|
121
142
|
try {
|
|
122
|
-
this.proc = spawn(
|
|
143
|
+
this.proc = spawn(this.houndExe!, [], {
|
|
123
144
|
stdio: ["pipe", "pipe", "pipe"],
|
|
124
145
|
windowsHide: true,
|
|
125
146
|
env: { ...process.env },
|
|
@@ -176,7 +197,7 @@ class HoundClient {
|
|
|
176
197
|
params: {
|
|
177
198
|
protocolVersion: "2025-03-26",
|
|
178
199
|
capabilities: {},
|
|
179
|
-
clientInfo: { name: "pi-hound", version:
|
|
200
|
+
clientInfo: { name: "pi-hound", version: EXTENSION_VERSION },
|
|
180
201
|
},
|
|
181
202
|
id,
|
|
182
203
|
}) + "\n",
|
|
@@ -215,10 +236,15 @@ class HoundClient {
|
|
|
215
236
|
} catch {}
|
|
216
237
|
}
|
|
217
238
|
|
|
218
|
-
async call(
|
|
239
|
+
async call(
|
|
240
|
+
name: string,
|
|
241
|
+
args: Record<string, any>,
|
|
242
|
+
timeoutMs = CALL_TIMEOUT_MS,
|
|
243
|
+
signal?: AbortSignal,
|
|
244
|
+
): Promise<any> {
|
|
219
245
|
const ready = await this.ensureReady();
|
|
220
246
|
if (!ready) {
|
|
221
|
-
const hint =
|
|
247
|
+
const hint = this.houndExe
|
|
222
248
|
? `Hound failed to start after ${INIT_ATTEMPTS} attempts. ${this.lastStderr || "Silent dead hang."} Run: hound --doctor`
|
|
223
249
|
: "Hound not found. Install: pip install hound-mcp[all]";
|
|
224
250
|
throw new Error(hint);
|
|
@@ -228,25 +254,44 @@ class HoundClient {
|
|
|
228
254
|
reject(new Error("Hound not running"));
|
|
229
255
|
return;
|
|
230
256
|
}
|
|
257
|
+
if (signal?.aborted) {
|
|
258
|
+
reject(new Error(`Hound cancelled: ${name}`));
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
231
261
|
const id = ++this.nextId;
|
|
232
|
-
|
|
262
|
+
let timer: NodeJS.Timeout;
|
|
263
|
+
let onAbort: () => void;
|
|
264
|
+
const cleanup = () => {
|
|
265
|
+
clearTimeout(timer);
|
|
233
266
|
this.pending.delete(id);
|
|
267
|
+
if (signal && onAbort) signal.removeEventListener("abort", onAbort);
|
|
268
|
+
};
|
|
269
|
+
timer = setTimeout(() => {
|
|
270
|
+
cleanup();
|
|
234
271
|
reject(new Error(`Hound timeout: ${name}`));
|
|
235
272
|
}, timeoutMs);
|
|
236
|
-
|
|
273
|
+
onAbort = () => {
|
|
274
|
+
cleanup();
|
|
275
|
+
reject(new Error(`Hound cancelled: ${name}`));
|
|
276
|
+
};
|
|
277
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
278
|
+
this.pending.set(id, {
|
|
279
|
+
resolve: (v: any) => { cleanup(); resolve(v); },
|
|
280
|
+
reject: (e: Error) => { cleanup(); reject(e); },
|
|
281
|
+
timer,
|
|
282
|
+
});
|
|
237
283
|
try {
|
|
238
284
|
this.proc.stdin!.write(
|
|
239
285
|
JSON.stringify({ jsonrpc: "2.0", method: "tools/call", params: { name, arguments: args }, id }) + "\n",
|
|
240
286
|
);
|
|
241
287
|
} catch (e) {
|
|
242
|
-
|
|
243
|
-
this.pending.delete(id);
|
|
288
|
+
cleanup();
|
|
244
289
|
reject(e as Error);
|
|
245
290
|
}
|
|
246
291
|
});
|
|
247
292
|
}
|
|
248
293
|
|
|
249
|
-
|
|
294
|
+
private _killProcess() {
|
|
250
295
|
if (this.proc && !this.proc.killed) {
|
|
251
296
|
try { this.proc.stdin?.end(); } catch {}
|
|
252
297
|
try { this.proc.kill(); } catch {}
|
|
@@ -257,6 +302,8 @@ class HoundClient {
|
|
|
257
302
|
for (const [, { timer }] of this.pending) clearTimeout(timer);
|
|
258
303
|
this.pending.clear();
|
|
259
304
|
}
|
|
305
|
+
|
|
306
|
+
kill() { this._killProcess(); }
|
|
260
307
|
}
|
|
261
308
|
|
|
262
309
|
const hound = new HoundClient();
|
|
@@ -288,7 +335,38 @@ function pick(params: Record<string, any>, keys: string[]): Record<string, any>
|
|
|
288
335
|
// -- Extension --
|
|
289
336
|
|
|
290
337
|
export default function (pi: ExtensionAPI) {
|
|
291
|
-
pi.on("session_start", (
|
|
338
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
339
|
+
const ok = await hound.ensureReady().catch(() => false);
|
|
340
|
+
if (!ok) {
|
|
341
|
+
if (ctx.hasUI) {
|
|
342
|
+
ctx.ui.notify(
|
|
343
|
+
hound.hasExe()
|
|
344
|
+
? "Hound found but failed to start. Run: hound --doctor"
|
|
345
|
+
: "Hound not found. Install: pip install hound-mcp[all]",
|
|
346
|
+
"warn",
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
// Best-effort version sync check (non-blocking)
|
|
352
|
+
if (ctx.hasUI && EXTENSION_VERSION !== "unknown") {
|
|
353
|
+
hound.call("version", {}, 5_000).then((result) => {
|
|
354
|
+
const parsed = tryJson(getText(result));
|
|
355
|
+
if (parsed.version) {
|
|
356
|
+
hound["houndVersion"] = parsed.version;
|
|
357
|
+
const extMajor = EXTENSION_VERSION.split(".")[0];
|
|
358
|
+
const hMajor = String(parsed.version).split(".")[0];
|
|
359
|
+
if (extMajor !== hMajor) {
|
|
360
|
+
ctx.ui.notify(
|
|
361
|
+
`Hound extension v${EXTENSION_VERSION} vs hound v${parsed.version} (major mismatch). Update: hound -u && pi update npm:@houndmcp/hound-mcp-pi`,
|
|
362
|
+
"warn",
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}).catch(() => {});
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
292
370
|
pi.on("session_shutdown", () => { hound.kill(); });
|
|
293
371
|
|
|
294
372
|
// -- web_fetch --
|
|
@@ -313,10 +391,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
313
391
|
actions: Type.Optional(Type.Array(Type.Object({}, { additionalProperties: true }), { description: "Page interactions on stealthy browser AFTER load, BEFORE extraction. Forces stealthy + bypasses cache. Each item: {click:'css'}, {fill:{selector:'css',text:'x'}}, {press:'Enter'}, {wait:500}, {scroll:3}, {wait_selector:'css'}." })),
|
|
314
392
|
options: Type.Optional(Type.Object({}, { additionalProperties: true, description: "include_links (bool,false: response.links=citations/navigation/external+primary_source), include_media (bool,false: up to 20 page image URLs), archive_fallback (bool,true: recover from Internet Archive on hard-block; false=raw failure), proxy, cookies, extra_headers, useragent, wait, network_idle, headless, respect_robots, real_chrome/solve_cloudflare/block_webrtc/hide_canvas/main_content_only/use_trafilatura (anti-detect tuning, good defaults, rarely needed)." })),
|
|
315
393
|
}),
|
|
316
|
-
async execute(_id, params,
|
|
394
|
+
async execute(_id, params, signal, _onUpdate, _ctx) {
|
|
317
395
|
try {
|
|
318
396
|
const args = pick(params, ["url", "urls", "extraction_type", "css_selector", "max_content_chars", "timeout", "cache_ttl", "force_fetcher", "offset", "pages", "password", "focus", "actions", "options"]);
|
|
319
|
-
const result = await hound.call("mcp_smart_fetch", args, CALL_TIMEOUT_MS);
|
|
397
|
+
const result = await hound.call("mcp_smart_fetch", args, CALL_TIMEOUT_MS, signal);
|
|
320
398
|
const text = getText(result);
|
|
321
399
|
const parsed = tryJson(text);
|
|
322
400
|
if (Array.isArray(parsed.results)) {
|
|
@@ -368,11 +446,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
368
446
|
query: Type.String({ description: "Search query" }),
|
|
369
447
|
options: Type.Optional(Type.Object({}, { additionalProperties: true, description: "max_results (1-50,6), cache_ttl (300), mode (auto|neural|find_similar), engines (list), site, exclude_sites, location, language, region, page, freshness, url (for find_similar)" })),
|
|
370
448
|
}),
|
|
371
|
-
async execute(_id, params,
|
|
449
|
+
async execute(_id, params, signal, _onUpdate, _ctx) {
|
|
372
450
|
try {
|
|
373
451
|
const args: Record<string, any> = { query: params.query };
|
|
374
452
|
if (params.options && Object.keys(params.options).length) args.options = params.options;
|
|
375
|
-
const result = await hound.call("mcp_smart_search", args, 60_000);
|
|
453
|
+
const result = await hound.call("mcp_smart_search", args, 60_000, signal);
|
|
376
454
|
const text = getText(result);
|
|
377
455
|
const parsed = tryJson(text);
|
|
378
456
|
const results: any[] = parsed.results ?? [];
|
|
@@ -425,10 +503,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
425
503
|
crawl_urls: Type.Optional(Type.Array(Type.String(), { description: "Chosen subset of URLs to fetch (second-phase selective crawl, no re-discovery). Use after sitemap=true or discover_only=true." })),
|
|
426
504
|
options: Type.Optional(Type.Object({}, { additionalProperties: true, description: "sitemap (true|'auto'|false,false: true=map from sitemap.xml in one fetch; 'auto'=use if present else BFS), max_pages (1-100,10), max_depth (0-5,2), path_include (list of path prefixes), path_exclude (list to skip), max_content_chars_per (8000), max_total_chars (token budget), concurrency (1-5,3), cache_ttl (3600;0=fresh), respect_robots (false), force_fetcher ('http'|'stealthy'), timeout (ms,30000), deadline_ms (120000)." })),
|
|
427
505
|
}),
|
|
428
|
-
async execute(_id, params,
|
|
506
|
+
async execute(_id, params, signal, _onUpdate, _ctx) {
|
|
429
507
|
try {
|
|
430
508
|
const args = pick(params, ["url", "focus", "discover_only", "crawl_urls", "options"]);
|
|
431
|
-
const result = await hound.call("mcp_smart_crawl", args, CRAWL_TIMEOUT_MS);
|
|
509
|
+
const result = await hound.call("mcp_smart_crawl", args, CRAWL_TIMEOUT_MS, signal);
|
|
432
510
|
const text = getText(result);
|
|
433
511
|
const parsed = tryJson(text);
|
|
434
512
|
const pages: any[] = Array.isArray(parsed.pages) ? parsed.pages : [];
|
|
@@ -474,16 +552,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
474
552
|
name: "web_screenshot",
|
|
475
553
|
label: "Web Screenshot",
|
|
476
554
|
description: "Screenshot a URL as an image. Multimodal agents only (content as images/canvas/visual layout). Text agents: use web_fetch. Stealthy browser auto-managed.",
|
|
477
|
-
promptSnippet: "web_screenshot(url, full_page, image_type) - anti-bot one-shot screenshot of a URL (stealthy browser). Multimodal agents only.",
|
|
555
|
+
promptSnippet: "web_screenshot(url, options.full_page, options.image_type) - anti-bot one-shot screenshot of a URL (stealthy browser). Multimodal agents only.",
|
|
478
556
|
parameters: Type.Object({
|
|
479
557
|
url: Type.String({ description: "URL to screenshot" }),
|
|
480
558
|
options: Type.Optional(Type.Object({}, { additionalProperties: true, description: "full_page (bool,false), image_type (png|jpeg,png), quality (0-100,jpeg), wait (ms), wait_selector (css), network_idle (bool), timeout (ms,30000)." })),
|
|
481
559
|
}),
|
|
482
|
-
async execute(_id, params,
|
|
560
|
+
async execute(_id, params, signal, _onUpdate, _ctx) {
|
|
483
561
|
try {
|
|
484
562
|
const args: Record<string, any> = { url: params.url };
|
|
485
563
|
if (params.options && Object.keys(params.options).length) args.options = params.options;
|
|
486
|
-
const result = await hound.call("mcp_screenshot", args, CALL_TIMEOUT_MS);
|
|
564
|
+
const result = await hound.call("mcp_screenshot", args, CALL_TIMEOUT_MS, signal);
|
|
487
565
|
const images = getImages(result);
|
|
488
566
|
if (images.length) {
|
|
489
567
|
const out: any[] = [];
|
|
@@ -523,10 +601,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
523
601
|
parameters: Type.Object({
|
|
524
602
|
all: Type.Optional(Type.Boolean({ description: "Wipe all (default: expired only)" })),
|
|
525
603
|
}),
|
|
526
|
-
async execute(_id, params,
|
|
604
|
+
async execute(_id, params, signal, _onUpdate, _ctx) {
|
|
527
605
|
try {
|
|
528
606
|
const args = { all: params.all ?? false };
|
|
529
|
-
const result = await hound.call("cache_clear", args, 10_000);
|
|
607
|
+
const result = await hound.call("cache_clear", args, 10_000, signal);
|
|
530
608
|
return { content: [{ type: "text", text: getText(result) }], details: { all: args.all } };
|
|
531
609
|
} catch (e: any) {
|
|
532
610
|
return { content: [{ type: "text", text: `cache_clear error: ${e.message}` }], details: { error: e.message } };
|
|
@@ -549,12 +627,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
549
627
|
description: "Hound version + update status.",
|
|
550
628
|
promptSnippet: "hound_version() - hound version + update status.",
|
|
551
629
|
parameters: Type.Object({}),
|
|
552
|
-
async execute(_id, _params,
|
|
630
|
+
async execute(_id, _params, signal, _onUpdate, _ctx) {
|
|
553
631
|
try {
|
|
554
|
-
const result = await hound.call("version", {}, 10_000);
|
|
555
|
-
|
|
632
|
+
const result = await hound.call("version", {}, 10_000, signal);
|
|
633
|
+
const parsed = tryJson(getText(result));
|
|
634
|
+
const v = parsed.version || "unknown";
|
|
635
|
+
const latest = parsed.latest || "";
|
|
636
|
+
const upToDate = parsed.up_to_date;
|
|
637
|
+
const extInfo = EXTENSION_VERSION !== "unknown" ? ` | extension v${EXTENSION_VERSION}` : "";
|
|
638
|
+
const text = upToDate
|
|
639
|
+
? `Hound ${v} (up to date)${extInfo}`
|
|
640
|
+
: `Hound ${v} (update available: ${latest})${extInfo}. Run: hound -u`;
|
|
641
|
+
return { content: [{ type: "text", text }], details: { version: v, latest, up_to_date: upToDate, extension: EXTENSION_VERSION } };
|
|
556
642
|
} catch (e: any) {
|
|
557
|
-
|
|
643
|
+
// Fallback: try hound --version via execSync (works even when MCP server is down)
|
|
644
|
+
try {
|
|
645
|
+
const exe = hound.getExe();
|
|
646
|
+
if (exe) {
|
|
647
|
+
const out = execSync(`"${exe}" --version`, { timeout: 5000, windowsHide: true }).toString().trim();
|
|
648
|
+
return { content: [{ type: "text", text: out }], details: { fallback: true, extension: EXTENSION_VERSION } };
|
|
649
|
+
}
|
|
650
|
+
} catch {}
|
|
651
|
+
return { content: [{ type: "text", text: `hound_version error: ${e.message}` }], details: { error: e.message, extension: EXTENSION_VERSION } };
|
|
558
652
|
}
|
|
559
653
|
},
|
|
560
654
|
renderCall(_args, theme) {
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@houndmcp/hound-mcp-pi",
|
|
3
|
-
"version": "10.3.
|
|
3
|
+
"version": "10.3.1",
|
|
4
4
|
"description": "Hound web research for Pi agent - keyless search, anti-bot fetch, deep crawl, screenshot, Internet Archive dead-link recovery. Free, MIT, no API key.",
|
|
5
5
|
"publishConfig": {"access": "public"},
|
|
6
|
+
"files": ["extensions/", "README.md"],
|
|
6
7
|
"keywords": ["pi-package", "mcp", "web", "search", "fetch", "crawl", "ai-agent", "scraping"],
|
|
7
8
|
"license": "MIT",
|
|
8
9
|
"author": "dondai1234",
|