ubersearch 1.6.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -69,8 +69,10 @@ ubersearch "query" [options]
69
69
  Options:
70
70
  --json Output results as JSON
71
71
  --engines engine1,engine2 Use specific engines
72
- --strategy all|first-success Search strategy (default: all)
72
+ --strategy all|first-success Search strategy (default: first-success)
73
73
  --limit number Max results per engine
74
+ --categories categories Categories filter for SearXNG (e.g., "it,science")
75
+ --config path Config file path
74
76
  --include-raw Include raw provider responses
75
77
  --help, -h Show help
76
78
  health Run provider health checks (starts Docker-backed ones if needed)
@@ -286,7 +288,7 @@ tavily (10 results)
286
288
 
287
289
  ## Search Strategies
288
290
 
289
- ### All (Default)
291
+ ### All
290
292
 
291
293
  Queries all configured/enabled providers and combines results.
292
294
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ubersearch",
3
- "version": "1.6.1",
3
+ "version": "1.7.0",
4
4
  "module": "src/index.ts",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,9 +41,9 @@
41
41
  },
42
42
  "scripts": {
43
43
  "prepare": "git config core.hooksPath scripts/hooks",
44
- "lint": "bunx biome check .",
45
- "lint:fix": "bunx biome check --write .",
46
- "format": "bunx biome format --write .",
44
+ "lint": "bunx biome check src/ test/ scripts/ docs/ *.json *.md",
45
+ "lint:fix": "bunx biome check --write src/ test/ scripts/ docs/ *.json *.md",
46
+ "format": "bunx biome format --write src/ test/ scripts/ docs/ *.json *.md",
47
47
  "test": "SKIP_DOCKER_TESTS=true bun test --preload ./test/setup.ts test/",
48
48
  "test:unit": "bun test --preload ./test/setup.ts test/unit/",
49
49
  "test:integration": "bun test --preload ./test/setup.ts test/integration/",
@@ -55,7 +55,7 @@
55
55
  "mcp": "bun run src/cli.ts mcp",
56
56
  "mcp:test": "bun run scripts/test-mcp.ts",
57
57
  "build": "bun run scripts/build.ts",
58
- "build:binary": "bun build --compile --minify src/cli.ts --outfile dist/ubersearch",
58
+ "build:binary": "bun build --compile --minify src/cli.ts --outfile dist/ubersearch && bun run scripts/copy-searxng-assets.ts",
59
59
  "semantic-release": "semantic-release",
60
60
  "semantic-release:dry-run": "semantic-release --dry-run",
61
61
  "release": "bun run build && bun run semantic-release"
@@ -76,6 +76,7 @@ search:
76
76
  # formats: [html, csv, json, rss]
77
77
  formats:
78
78
  - html
79
+ - json
79
80
 
80
81
  server:
81
82
  # Is overwritten by ${SEARXNG_PORT} and ${SEARXNG_BIND_ADDRESS}
@@ -94,7 +95,7 @@ server:
94
95
  # If your instance owns a /etc/searxng/settings.yml file, then set the following
95
96
  # values there.
96
97
 
97
- secret_key: "ultrasecretkey" # Is overwritten by ${SEARXNG_SECRET}
98
+ secret_key: "5d12e53bd34ee628ed8f8f90d796a8ea3f5640eb9b381d2b58945f2a0d210468" # Is overwritten by ${SEARXNG_SECRET}
98
99
  # Proxy image results through SearXNG. Is overwritten by ${SEARXNG_IMAGE_PROXY}
99
100
  image_proxy: false
100
101
  # 1.0 and 1.1 are supported
@@ -9,7 +9,7 @@ services:
9
9
  environment:
10
10
  # SearXNG configuration options
11
11
  - SEARXNG_BIND_ADDRESS=0.0.0.0:8080
12
- - SEARXNG_SECRET_KEY=${SEARXNG_SECRET_KEY:-changeme}
12
+ - SEARXNG_SECRET=${SEARXNG_SECRET}
13
13
  - SEARXNG_DEBUG=false
14
14
  # Enable JSON API
15
15
  - SEARXNG_ENABLE_JSON_API=true
@@ -11,7 +11,7 @@ import { CreditManager } from "../core/credits";
11
11
  import { FileCreditStateProvider } from "../core/credits/FileCreditStateProvider";
12
12
  import { createLogger } from "../core/logger";
13
13
  import { UberSearchOrchestrator } from "../core/orchestrator";
14
- import type { ILifecycleProvider, SearchProvider } from "../core/provider";
14
+ import type { SearchProvider } from "../core/provider";
15
15
  import { ProviderRegistry } from "../core/provider";
16
16
  import { ProviderFactory } from "../core/provider/ProviderFactory";
17
17
  import { ServiceKeys } from "../core/serviceKeys";
@@ -156,17 +156,3 @@ export async function bootstrapContainer(
156
156
  function createProvider(engineConfig: EngineConfig): SearchProvider {
157
157
  return ProviderFactory.createProvider(engineConfig, container);
158
158
  }
159
-
160
- /**
161
- * Helper function to check if a provider implements ILifecycleProvider
162
- */
163
- export function isLifecycleProvider(provider: unknown): provider is ILifecycleProvider {
164
- return (
165
- provider != null &&
166
- typeof provider === "object" &&
167
- "init" in provider &&
168
- typeof provider.init === "function" &&
169
- "healthcheck" in provider &&
170
- typeof provider.healthcheck === "function"
171
- );
172
- }
package/src/cli.ts CHANGED
@@ -5,10 +5,11 @@
5
5
  * Unified search interface for multiple search providers
6
6
  */
7
7
 
8
- import { bootstrapContainer, isLifecycleProvider } from "./bootstrap/container";
8
+ import { bootstrapContainer } from "./bootstrap/container";
9
9
  import type { ProviderRegistry } from "./core/provider";
10
10
  import { ServiceKeys } from "./core/serviceKeys";
11
11
  import { serve as serveMcp } from "./mcp-server";
12
+ import { isLifecycleProvider } from "./plugin/types.js";
12
13
  import type { UberSearchOutput } from "./tool/interface";
13
14
  import { getCreditStatus, uberSearch } from "./tool/uberSearchTool";
14
15
 
@@ -14,28 +14,11 @@
14
14
  import { existsSync, readFileSync } from "node:fs";
15
15
  import { homedir } from "node:os";
16
16
  import { dirname, isAbsolute, join } from "node:path";
17
- import { fileURLToPath } from "node:url";
17
+ import { getBundledSearxngComposePath } from "../core/paths";
18
18
  import { PluginRegistry, registerBuiltInPlugins } from "../plugin";
19
19
  import type { ConfigFactory, ExtendedSearchConfig } from "./defineConfig";
20
20
  import { formatValidationErrors, validateConfigSafe } from "./validation";
21
21
 
22
- /**
23
- * Get package root directory
24
- * Handles both development (src/) and bundled (dist/) environments
25
- */
26
- function getPackageRoot(): string {
27
- const currentFile = fileURLToPath(import.meta.url);
28
- const currentDir = dirname(currentFile);
29
-
30
- // Check if we're in dist/ or src/
31
- if (currentDir.includes("/dist")) {
32
- // Bundled: dist/cli.js -> go up 1 level
33
- return dirname(currentDir);
34
- }
35
- // Development: src/config/load.ts -> go up 2 levels
36
- return dirname(dirname(currentDir));
37
- }
38
-
39
22
  /** Supported config file extensions */
40
23
  const CONFIG_EXTENSIONS = [".ts", ".json"] as const;
41
24
 
@@ -210,8 +193,7 @@ function getDefaultConfig(): ExtendedSearchConfig {
210
193
  const defaultEngineOrder: string[] = [];
211
194
 
212
195
  // Always add SearXNG first - it's free and unlimited (requires Docker)
213
- const packageRoot = getPackageRoot();
214
- const composeFile = join(packageRoot, "providers", "searxng", "docker-compose.yml");
196
+ const composeFile = getBundledSearxngComposePath();
215
197
 
216
198
  defaultEngineOrder.push("searchxng");
217
199
  engines.push({
@@ -100,6 +100,24 @@ export class CreditManager {
100
100
  return result;
101
101
  }
102
102
 
103
+ /**
104
+ * Refund credits for a failed search
105
+ * @note This does NOT persist the state - call saveState() separately
106
+ */
107
+ refund(engineId: EngineId): void {
108
+ const config = this.engines.get(engineId);
109
+ if (!config) {
110
+ throw new Error(`Unknown engine: ${engineId}`);
111
+ }
112
+
113
+ const record = this.state[engineId];
114
+ if (!record) {
115
+ throw new Error(`No credit record for engine: ${engineId}`);
116
+ }
117
+
118
+ record.used = Math.max(0, record.used - config.creditCostPerSearch);
119
+ }
120
+
103
121
  /**
104
122
  * Check if engine has sufficient credits
105
123
  */
@@ -4,12 +4,40 @@
4
4
  * Manages Docker Compose services for local providers
5
5
  */
6
6
 
7
- import { exec } from "node:child_process";
8
- import { existsSync } from "node:fs";
7
+ import { execFile } from "node:child_process";
8
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
9
+ import { join } from "node:path";
9
10
  import { promisify } from "node:util";
10
11
  import { getSearxngPaths } from "../paths";
11
12
 
12
- const execAsync = promisify(exec);
13
+ const execFileAsync = promisify(execFile);
14
+
15
+ /**
16
+ * Get or generate a persistent SearXNG secret key
17
+ * Stored in the config directory so it persists across restarts
18
+ */
19
+ function getSearxngSecret(configDir: string): string {
20
+ const secretFile = join(configDir, ".secret");
21
+ try {
22
+ if (existsSync(secretFile)) {
23
+ const secret = readFileSync(secretFile, "utf-8").trim();
24
+ if (secret.length >= 32) {
25
+ return secret;
26
+ }
27
+ }
28
+ } catch {
29
+ // Fall through to generate new secret
30
+ }
31
+
32
+ // Generate a new secret using crypto
33
+ const secret = [...Array(64)].map(() => Math.floor(Math.random() * 16).toString(16)).join("");
34
+ try {
35
+ writeFileSync(secretFile, secret, { mode: 0o600 });
36
+ } catch {
37
+ // If we can't write, use a session-only secret
38
+ }
39
+ return secret;
40
+ }
13
41
 
14
42
  export interface DockerComposeOptions {
15
43
  cwd?: string;
@@ -29,21 +57,22 @@ export class DockerComposeHelper {
29
57
  args: string[],
30
58
  options: DockerComposeOptions = {},
31
59
  ): Promise<string> {
32
- const cmd = `docker compose -f "${this.composeFile}" ${args.join(" ")}`;
33
60
  const cwd = options.cwd || this.getComposeDir();
34
61
  const timeout = options.timeout || 30000;
62
+ const cmdArgs = ["compose", "-f", this.composeFile, ...args];
35
63
 
36
64
  try {
37
65
  // Get SearXNG paths and ensure directories exist
38
66
  const { configDir, dataDir } = getSearxngPaths();
39
67
 
40
- const { stdout, stderr } = await execAsync(cmd, {
68
+ const { stdout, stderr } = await execFileAsync("docker", cmdArgs, {
41
69
  cwd,
42
70
  timeout,
43
71
  env: {
44
72
  ...process.env,
45
73
  SEARXNG_CONFIG: configDir,
46
74
  SEARXNG_DATA: dataDir,
75
+ SEARXNG_SECRET: getSearxngSecret(configDir),
47
76
  },
48
77
  });
49
78
 
@@ -59,7 +88,7 @@ export class DockerComposeHelper {
59
88
 
60
89
  const errorDetails = [
61
90
  `Docker Compose command ${timedOut ? "timed out" : "failed"}: ${errorMessage}`,
62
- `Command: ${cmd}`,
91
+ `Command: docker compose -f ${this.composeFile} ${args.join(" ")}`,
63
92
  ];
64
93
  if (err.stdout) {
65
94
  errorDetails.push(`Output: ${err.stdout}`);
@@ -161,7 +190,7 @@ export class DockerComposeHelper {
161
190
  */
162
191
  static async isDockerAvailable(): Promise<boolean> {
163
192
  try {
164
- await execAsync("docker version", { timeout: 5000 });
193
+ await execFileAsync("docker", ["version"], { timeout: 5000 });
165
194
  return true;
166
195
  } catch {
167
196
  return false;
@@ -63,21 +63,20 @@ export class DockerLifecycleManager {
63
63
  * @throws Error if container startup fails
64
64
  */
65
65
  async init(): Promise<void> {
66
- if (this.initPromise) {
67
- return this.initPromise;
68
- }
66
+ // Atomic check-and-set pattern to prevent race conditions
67
+ if (!this.initPromise) {
68
+ if (!this.config.autoStart || !this.dockerHelper) {
69
+ this.initialized = true;
70
+ return;
71
+ }
69
72
 
70
- if (!this.config.autoStart || !this.dockerHelper) {
71
- this.initialized = true;
72
- return;
73
+ this.initPromise = this.performInit().catch((error) => {
74
+ const message = error instanceof Error ? error.message : String(error);
75
+ log.debug("Initialization failed:", message);
76
+ this.initialized = false;
77
+ throw error;
78
+ });
73
79
  }
74
-
75
- this.initPromise = this.performInit().catch((error) => {
76
- const message = error instanceof Error ? error.message : String(error);
77
- log.debug("Initialization failed:", message);
78
- this.initialized = false;
79
- throw error;
80
- });
81
80
  return this.initPromise;
82
81
  }
83
82
 
@@ -162,20 +161,20 @@ export class DockerLifecycleManager {
162
161
  // Try health endpoint first - works for any running instance
163
162
  // (even if started manually, via different docker-compose, or k8s, etc.)
164
163
  if (this.config.healthEndpoint) {
164
+ const controller = new AbortController();
165
+ const timeoutId = setTimeout(() => controller.abort(), 3000);
165
166
  try {
166
- const controller = new AbortController();
167
- const timeoutId = setTimeout(() => controller.abort(), 3000);
168
-
169
167
  const response = await fetch(this.config.healthEndpoint, {
170
168
  signal: controller.signal,
171
169
  });
172
170
 
173
- clearTimeout(timeoutId);
174
171
  if (response.ok) {
175
172
  return true;
176
173
  }
177
174
  } catch {
178
175
  // Health endpoint failed, fall through to Docker check
176
+ } finally {
177
+ clearTimeout(timeoutId);
179
178
  }
180
179
  }
181
180
 
@@ -269,6 +268,23 @@ export class DockerLifecycleManager {
269
268
  const errors: string[] = [];
270
269
  const warnings: string[] = [];
271
270
 
271
+ // Validate config fields first (these don't require Docker)
272
+
273
+ // Check health endpoint if configured
274
+ if (this.config.healthEndpoint) {
275
+ try {
276
+ // Try to parse the URL
277
+ new URL(this.config.healthEndpoint);
278
+ } catch {
279
+ warnings.push(`Health endpoint URL is invalid: ${this.config.healthEndpoint}`);
280
+ }
281
+ }
282
+
283
+ // Check container name if specified
284
+ if (this.config.containerName && !/^[a-zA-Z0-9_-]+$/.test(this.config.containerName)) {
285
+ warnings.push(`Container name contains invalid characters: ${this.config.containerName}`);
286
+ }
287
+
272
288
  // Check Docker is available
273
289
  const dockerAvailable = await DockerComposeHelper.isDockerAvailable();
274
290
  if (!dockerAvailable) {
@@ -297,21 +313,6 @@ export class DockerLifecycleManager {
297
313
  }
298
314
  }
299
315
 
300
- // Check health endpoint if configured
301
- if (this.config.healthEndpoint) {
302
- try {
303
- // Try to parse the URL
304
- new URL(this.config.healthEndpoint);
305
- } catch {
306
- warnings.push(`Health endpoint URL is invalid: ${this.config.healthEndpoint}`);
307
- }
308
- }
309
-
310
- // Check container name if specified
311
- if (this.config.containerName && !/^[a-zA-Z0-9_-]+$/.test(this.config.containerName)) {
312
- warnings.push(`Container name contains invalid characters: ${this.config.containerName}`);
313
- }
314
-
315
316
  return {
316
317
  valid: errors.length === 0,
317
318
  errors,
@@ -333,6 +334,13 @@ export class DockerLifecycleManager {
333
334
  return this.initialized;
334
335
  }
335
336
 
337
+ /**
338
+ * Check if initialization is currently in progress
339
+ */
340
+ isInitializing(): boolean {
341
+ return this.initPromise !== null;
342
+ }
343
+
336
344
  /**
337
345
  * Check if container is running
338
346
  *
package/src/core/paths.ts CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { copyFileSync, existsSync, mkdirSync } from "node:fs";
9
9
  import { homedir } from "node:os";
10
- import { dirname, join } from "node:path";
10
+ import { dirname, join, resolve } from "node:path";
11
11
  import { fileURLToPath } from "node:url";
12
12
 
13
13
  const APP_NAME = "ubersearch";
@@ -108,17 +108,58 @@ export function getPackageRoot(): string {
108
108
  return dirname(dirname(currentDir));
109
109
  }
110
110
 
111
+ /**
112
+ * Get candidate runtime roots where bundled SearXNG assets may live.
113
+ *
114
+ * This covers:
115
+ * - source checkouts (`<root>/providers/searxng`)
116
+ * - built JS output (`<root>/dist/providers/searxng`)
117
+ * - compiled binaries with copied assets next to the executable
118
+ */
119
+ function getRuntimeRoots(): string[] {
120
+ const roots = new Set<string>();
121
+
122
+ roots.add(resolve(getPackageRoot()));
123
+ roots.add(resolve(dirname(process.execPath)));
124
+ roots.add(resolve(process.cwd()));
125
+
126
+ return [...roots];
127
+ }
128
+
129
+ function getBundledSearxngAssetCandidates(relativePath: string): string[] {
130
+ const candidates = new Set<string>();
131
+
132
+ for (const root of getRuntimeRoots()) {
133
+ candidates.add(join(root, "providers", "searxng", relativePath));
134
+ candidates.add(join(root, "dist", "providers", "searxng", relativePath));
135
+ }
136
+
137
+ return [...candidates];
138
+ }
139
+
140
+ function resolveBundledSearxngAsset(relativePath: string): string {
141
+ const candidates = getBundledSearxngAssetCandidates(relativePath);
142
+ const existing = candidates.find((candidate) => existsSync(candidate));
143
+
144
+ if (existing) {
145
+ return existing;
146
+ }
147
+
148
+ return candidates[0] ?? join(getPackageRoot(), "providers", "searxng", relativePath);
149
+ }
150
+
151
+ /**
152
+ * Get the bundled SearXNG docker-compose file path.
153
+ */
154
+ export function getBundledSearxngComposePath(): string {
155
+ return resolveBundledSearxngAsset("docker-compose.yml");
156
+ }
157
+
111
158
  /**
112
159
  * Get the bundled default settings.yml path
113
160
  */
114
161
  export function getDefaultSettingsPath(): string {
115
- const packageRoot = getPackageRoot();
116
- // In bundled mode, look in dist/providers/searxng/
117
- // In dev mode, look in providers/searxng/
118
- if (packageRoot.endsWith("/dist")) {
119
- return join(packageRoot, "providers", "searxng", "config", "settings.yml");
120
- }
121
- return join(packageRoot, "providers", "searxng", "config", "settings.yml");
162
+ return resolveBundledSearxngAsset(join("config", "settings.yml"));
122
163
  }
123
164
 
124
165
  /**
@@ -81,8 +81,8 @@ export class AllProvidersStrategy implements ISearchStrategy {
81
81
  continue;
82
82
  }
83
83
 
84
- // Check credit availability
85
- if (!context.creditManager.hasSufficientCredits(engineId)) {
84
+ // Charge credits before search (atomic check+deduct to avoid race conditions)
85
+ if (!context.creditManager.charge(engineId)) {
86
86
  attempts.push({ engineId, success: false, reason: "out_of_credit" });
87
87
  continue;
88
88
  }
@@ -98,13 +98,6 @@ export class AllProvidersStrategy implements ISearchStrategy {
98
98
  }),
99
99
  );
100
100
 
101
- // Deduct credits
102
- if (!context.creditManager.charge(engineId)) {
103
- // This shouldn't happen because we checked above, but handle gracefully
104
- attempts.push({ engineId, success: false, reason: "out_of_credit" });
105
- continue;
106
- }
107
-
108
101
  // Record success
109
102
  attempts.push({ engineId, success: true });
110
103
 
@@ -115,6 +108,9 @@ export class AllProvidersStrategy implements ISearchStrategy {
115
108
  results.push(...response.items);
116
109
  }
117
110
  } catch (error) {
111
+ // Refund credits for failed search
112
+ context.creditManager.refund(engineId);
113
+
118
114
  // Record failure
119
115
  if (error instanceof SearchError) {
120
116
  attempts.push({ engineId, success: false, reason: error.reason });
@@ -163,7 +159,8 @@ export class AllProvidersStrategy implements ISearchStrategy {
163
159
  continue;
164
160
  }
165
161
 
166
- if (!context.creditManager.hasSufficientCredits(engineId)) {
162
+ // Charge credits before search (atomic check+deduct to avoid race conditions)
163
+ if (!context.creditManager.charge(engineId)) {
167
164
  ineligibleAttempts.push({ engineId, success: false, reason: "out_of_credit" });
168
165
  continue;
169
166
  }
@@ -215,6 +212,9 @@ export class AllProvidersStrategy implements ISearchStrategy {
215
212
  const { response, error } = settledResult.value;
216
213
 
217
214
  if (error) {
215
+ // Refund credits for failed search
216
+ context.creditManager.refund(engineId);
217
+
218
218
  // Search threw an error
219
219
  if (error instanceof SearchError) {
220
220
  attempts.push({ engineId, success: false, reason: error.reason });
@@ -226,12 +226,6 @@ export class AllProvidersStrategy implements ISearchStrategy {
226
226
  }
227
227
 
228
228
  if (response) {
229
- // Deduct credits
230
- if (!context.creditManager.charge(engineId)) {
231
- attempts.push({ engineId, success: false, reason: "out_of_credit" });
232
- continue;
233
- }
234
-
235
229
  // Record success
236
230
  attempts.push({ engineId, success: true });
237
231
 
@@ -53,8 +53,8 @@ export class FirstSuccessStrategy implements ISearchStrategy {
53
53
  continue;
54
54
  }
55
55
 
56
- // Check credit availability
57
- if (!context.creditManager.hasSufficientCredits(engineId)) {
56
+ // Charge credits before search (atomic check+deduct to avoid race conditions)
57
+ if (!context.creditManager.charge(engineId)) {
58
58
  attempts.push({ engineId, success: false, reason: "out_of_credit" });
59
59
  continue;
60
60
  }
@@ -70,18 +70,15 @@ export class FirstSuccessStrategy implements ISearchStrategy {
70
70
  }),
71
71
  );
72
72
 
73
- // Deduct credits
74
- if (!context.creditManager.charge(engineId)) {
75
- attempts.push({ engineId, success: false, reason: "out_of_credit" });
76
- continue;
77
- }
78
-
79
73
  // Record success
80
74
  attempts.push({ engineId, success: true });
81
75
 
82
76
  // Return immediately with results from this provider
83
77
  return { results: response.items, attempts };
84
78
  } catch (error) {
79
+ // Refund credits for failed search
80
+ context.creditManager.refund(engineId);
81
+
85
82
  // Record failure and continue to next provider
86
83
  if (error instanceof SearchError) {
87
84
  attempts.push({ engineId, success: false, reason: error.reason });
package/src/mcp-server.ts CHANGED
@@ -7,32 +7,27 @@
7
7
  */
8
8
 
9
9
  import { bootstrapContainer, getCreditStatus, uberSearch } from "./app/index";
10
- import { isLifecycleProvider } from "./bootstrap/container";
11
10
  import type { ProviderRegistry } from "./core/provider";
12
11
  import { ServiceKeys } from "./core/serviceKeys";
12
+ import { isLifecycleProvider } from "./plugin/types.js";
13
13
 
14
14
  interface MCPRequest {
15
15
  jsonrpc: string;
16
16
  id?: number | string;
17
17
  method: string;
18
- params?: any;
18
+ params?: Record<string, unknown>;
19
19
  }
20
20
 
21
21
  interface MCPTool {
22
22
  name: string;
23
23
  description: string;
24
- inputSchema: any;
25
- }
26
-
27
- interface MCPToolCallParams {
28
- name: string;
29
- arguments: Record<string, any>;
24
+ inputSchema: Record<string, unknown>;
30
25
  }
31
26
 
32
27
  interface MCPResponse {
33
28
  jsonrpc: string;
34
29
  id?: number | string;
35
- result?: any;
30
+ result?: unknown;
36
31
  error?: {
37
32
  code?: number;
38
33
  message: string;
@@ -168,23 +163,29 @@ Example: "it,science" for tech and academic results`,
168
163
  }
169
164
 
170
165
  if (request.method === "tools/call") {
171
- const { name, arguments: args } = request.params as MCPToolCallParams;
166
+ const params = request.params ?? {};
167
+ const name = String(params.name ?? "");
168
+ const args =
169
+ typeof params.arguments === "object" && params.arguments !== null
170
+ ? (params.arguments as Record<string, string>)
171
+ : ({} as Record<string, string>);
172
172
 
173
173
  try {
174
- let result: any;
174
+ let result: unknown;
175
175
  if (name === "uber_search") {
176
- const engines = args.engines
177
- ? args.engines.split(",").map((e: string) => e.trim())
178
- : undefined;
176
+ const engines = args.engines ? args.engines.split(",").map((e) => e.trim()) : undefined;
179
177
  const categories = args.categories
180
- ? args.categories.split(",").map((c: string) => c.trim())
178
+ ? args.categories.split(",").map((c) => c.trim())
181
179
  : undefined;
182
180
  result = await withTimeout(
183
181
  uberSearch({
184
- query: args.query,
185
- limit: args.limit,
182
+ query: args.query ?? "",
183
+ limit: args.limit ? Number(args.limit) : undefined,
186
184
  engines,
187
- strategy: args.strategy,
185
+ strategy:
186
+ args.strategy === "all" || args.strategy === "first-success"
187
+ ? args.strategy
188
+ : undefined,
188
189
  categories,
189
190
  }),
190
191
  60000,
@@ -79,7 +79,7 @@ export class SearchxngProvider
79
79
  let isHealthy = await this.healthcheck();
80
80
 
81
81
  if (!isHealthy) {
82
- const isInitializing = !!(await (this.lifecycleManager as any).initPromise);
82
+ const isInitializing = this.lifecycleManager.isInitializing();
83
83
  if (!isInitializing) {
84
84
  try {
85
85
  log.debug("Container not healthy, attempting auto-start...");