@evantahler/mcpx 0.18.3 → 0.18.6

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.
Files changed (53) hide show
  1. package/package.json +63 -63
  2. package/src/cli.ts +46 -54
  3. package/src/client/browser.ts +36 -15
  4. package/src/client/debug-fetch.ts +64 -56
  5. package/src/client/elicitation.ts +279 -291
  6. package/src/client/http.ts +1 -1
  7. package/src/client/manager.ts +481 -514
  8. package/src/client/oauth.ts +272 -282
  9. package/src/client/sse.ts +1 -1
  10. package/src/client/stdio.ts +7 -7
  11. package/src/client/trace.ts +146 -152
  12. package/src/client/transport-options.ts +20 -20
  13. package/src/commands/add.ts +160 -165
  14. package/src/commands/allow.ts +141 -142
  15. package/src/commands/auth.ts +86 -90
  16. package/src/commands/check-update.ts +49 -53
  17. package/src/commands/deny.ts +114 -117
  18. package/src/commands/exec.ts +218 -222
  19. package/src/commands/index.ts +41 -41
  20. package/src/commands/info.ts +48 -50
  21. package/src/commands/list.ts +49 -49
  22. package/src/commands/ping.ts +47 -50
  23. package/src/commands/prompt.ts +40 -50
  24. package/src/commands/remove.ts +54 -56
  25. package/src/commands/resource.ts +31 -36
  26. package/src/commands/search.ts +35 -39
  27. package/src/commands/servers.ts +44 -48
  28. package/src/commands/skill.ts +89 -95
  29. package/src/commands/task.ts +50 -60
  30. package/src/commands/upgrade.ts +191 -208
  31. package/src/commands/with-command.ts +27 -29
  32. package/src/config/env.ts +26 -28
  33. package/src/config/loader.ts +103 -103
  34. package/src/config/schemas.ts +78 -87
  35. package/src/constants.ts +17 -17
  36. package/src/context.ts +51 -51
  37. package/src/lib/client-settings.ts +127 -140
  38. package/src/lib/input.ts +23 -26
  39. package/src/output/format-output.ts +12 -16
  40. package/src/output/format-table.ts +39 -42
  41. package/src/output/formatter.ts +794 -815
  42. package/src/output/logger.ts +140 -152
  43. package/src/sdk.ts +283 -291
  44. package/src/search/index.ts +50 -54
  45. package/src/search/indexer.ts +65 -65
  46. package/src/search/keyword.ts +54 -54
  47. package/src/search/semantic.ts +39 -39
  48. package/src/search/staleness.ts +3 -3
  49. package/src/search/types.ts +4 -4
  50. package/src/update/background.ts +51 -51
  51. package/src/update/cache.ts +21 -21
  52. package/src/update/checker.ts +81 -86
  53. package/src/validation/schema.ts +53 -58
@@ -1,159 +1,159 @@
1
- import { join, resolve } from "path";
2
- import { interpolateEnv } from "./env.ts";
1
+ import { chmod } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
3
  import { DEFAULT_CONFIG_DIR, ENV } from "../constants.ts";
4
+ import { interpolateEnv } from "./env.ts";
4
5
  import {
5
- type Config,
6
- type ServersFile,
7
- type AuthFile,
8
- type SearchIndex,
9
- validateServersFile,
10
- validateAuthFile,
11
- validateSearchIndex,
6
+ type AuthFile,
7
+ type Config,
8
+ type SearchIndex,
9
+ type ServersFile,
10
+ validateAuthFile,
11
+ validateSearchIndex,
12
+ validateServersFile,
12
13
  } from "./schemas.ts";
13
14
 
14
15
  const EMPTY_SERVERS: ServersFile = { mcpServers: {} };
15
16
  const EMPTY_AUTH: AuthFile = {};
16
17
  const EMPTY_SEARCH_INDEX: SearchIndex = {
17
- version: 1,
18
- indexed_at: "",
19
- embedding_model: "claude",
20
- tools: [],
18
+ version: 1,
19
+ indexed_at: "",
20
+ embedding_model: "claude",
21
+ tools: [],
21
22
  };
22
23
 
23
24
  /** Read and parse a JSON file, returning undefined if it doesn't exist */
24
25
  async function readJsonFile(path: string): Promise<unknown | undefined> {
25
- const file = Bun.file(path);
26
- if (!(await file.exists())) return undefined;
27
- const text = await file.text();
28
- return JSON.parse(text);
26
+ const file = Bun.file(path);
27
+ if (!(await file.exists())) return undefined;
28
+ const text = await file.text();
29
+ return JSON.parse(text);
29
30
  }
30
31
 
31
32
  /** Resolve the config directory from options, env, cwd, or default */
32
33
  function resolveConfigDir(configFlag?: string): string {
33
- // 1. -c / --config flag
34
- if (configFlag) return resolve(configFlag);
34
+ // 1. -c / --config flag
35
+ if (configFlag) return resolve(configFlag);
35
36
 
36
- // 2. MCP_CONFIG_PATH env var
37
- const envPath = process.env[ENV.CONFIG_PATH];
38
- if (envPath) return resolve(envPath);
37
+ // 2. MCP_CONFIG_PATH env var
38
+ const envPath = process.env[ENV.CONFIG_PATH];
39
+ if (envPath) return resolve(envPath);
39
40
 
40
- // 3. ./servers.json exists in cwd → use cwd
41
- // (checked at load time, not here — we return the candidate dir)
41
+ // 3. ./servers.json exists in cwd → use cwd
42
+ // (checked at load time, not here — we return the candidate dir)
42
43
 
43
- // 4. Default ~/.mcpx/
44
- return DEFAULT_CONFIG_DIR;
44
+ // 4. Default ~/.mcpx/
45
+ return DEFAULT_CONFIG_DIR;
45
46
  }
46
47
 
47
48
  /** Check if servers.json exists in the given directory */
48
49
  async function hasServersFile(dir: string): Promise<boolean> {
49
- return Bun.file(join(dir, "servers.json")).exists();
50
+ return Bun.file(join(dir, "servers.json")).exists();
50
51
  }
51
52
 
52
53
  export interface LoadConfigOptions {
53
- configFlag?: string;
54
+ configFlag?: string;
54
55
  }
55
56
 
56
57
  /** Load and validate all config files */
57
58
  export async function loadConfig(options: LoadConfigOptions = {}): Promise<Config> {
58
- // Resolve config directory
59
- let configDir = resolveConfigDir(options.configFlag);
60
-
61
- // If the resolved dir doesn't have servers.json, check cwd
62
- if (!(await hasServersFile(configDir))) {
63
- const cwd = process.cwd();
64
- if (await hasServersFile(cwd)) {
65
- configDir = cwd;
66
- }
67
- }
68
-
69
- // Ensure config directory exists
70
- await Bun.write(join(configDir, ".keep"), "").catch(() => {});
71
- // Remove the .keep file, it was just to ensure the dir
72
- Bun.file(join(configDir, ".keep"))
73
- .exists()
74
- .then((exists) => {
75
- if (exists) Bun.write(join(configDir, ".keep"), "");
76
- });
77
-
78
- // Load servers.json
79
- const serversPath = join(configDir, "servers.json");
80
- const rawServers = await readJsonFile(serversPath);
81
- let servers: ServersFile;
82
- if (rawServers === undefined) {
83
- servers = EMPTY_SERVERS;
84
- } else {
85
- servers = validateServersFile(rawServers);
86
- // Interpolate env vars in server configs
87
- servers = {
88
- mcpServers: Object.fromEntries(
89
- Object.entries(servers.mcpServers).map(([name, config]) => [name, interpolateEnv(config)]),
90
- ),
91
- };
92
- }
93
-
94
- // Load auth.json
95
- const authPath = join(configDir, "auth.json");
96
- const rawAuth = await readJsonFile(authPath);
97
- const auth: AuthFile = rawAuth !== undefined ? validateAuthFile(rawAuth) : EMPTY_AUTH;
98
-
99
- // Load search.json
100
- const searchPath = join(configDir, "search.json");
101
- const rawSearch = await readJsonFile(searchPath);
102
- const searchIndex: SearchIndex =
103
- rawSearch !== undefined ? validateSearchIndex(rawSearch) : EMPTY_SEARCH_INDEX;
104
-
105
- return { configDir, servers, auth, searchIndex };
59
+ // Resolve config directory
60
+ let configDir = resolveConfigDir(options.configFlag);
61
+
62
+ // If the resolved dir doesn't have servers.json, check cwd
63
+ if (!(await hasServersFile(configDir))) {
64
+ const cwd = process.cwd();
65
+ if (await hasServersFile(cwd)) {
66
+ configDir = cwd;
67
+ process.stderr.write(`Note: using servers.json from current directory (${cwd})\n`);
68
+ }
69
+ }
70
+
71
+ // Ensure config directory exists
72
+ await Bun.write(join(configDir, ".keep"), "").catch(() => {});
73
+ // Remove the .keep file, it was just to ensure the dir
74
+ Bun.file(join(configDir, ".keep"))
75
+ .exists()
76
+ .then((exists) => {
77
+ if (exists) Bun.write(join(configDir, ".keep"), "");
78
+ });
79
+
80
+ // Load servers.json
81
+ const serversPath = join(configDir, "servers.json");
82
+ const rawServers = await readJsonFile(serversPath);
83
+ let servers: ServersFile;
84
+ if (rawServers === undefined) {
85
+ servers = EMPTY_SERVERS;
86
+ } else {
87
+ servers = validateServersFile(rawServers);
88
+ // Interpolate env vars in server configs
89
+ servers = {
90
+ mcpServers: Object.fromEntries(
91
+ Object.entries(servers.mcpServers).map(([name, config]) => [name, interpolateEnv(config)]),
92
+ ),
93
+ };
94
+ }
95
+
96
+ // Load auth.json
97
+ const authPath = join(configDir, "auth.json");
98
+ const rawAuth = await readJsonFile(authPath);
99
+ const auth: AuthFile = rawAuth !== undefined ? validateAuthFile(rawAuth) : EMPTY_AUTH;
100
+
101
+ // Load search.json
102
+ const searchPath = join(configDir, "search.json");
103
+ const rawSearch = await readJsonFile(searchPath);
104
+ const searchIndex: SearchIndex = rawSearch !== undefined ? validateSearchIndex(rawSearch) : EMPTY_SEARCH_INDEX;
105
+
106
+ return { configDir, servers, auth, searchIndex };
106
107
  }
107
108
 
108
109
  /** Write a JSON file to the config directory */
109
110
  async function saveJsonFile(configDir: string, filename: string, data: unknown): Promise<void> {
110
- await Bun.write(join(configDir, filename), JSON.stringify(data, null, 2) + "\n");
111
+ await Bun.write(join(configDir, filename), `${JSON.stringify(data, null, 2)}\n`);
111
112
  }
112
113
 
113
- /** Save auth.json to the config directory */
114
+ /** Save auth.json to the config directory with restrictive permissions */
114
115
  export async function saveAuth(configDir: string, auth: AuthFile): Promise<void> {
115
- return saveJsonFile(configDir, "auth.json", auth);
116
+ await saveJsonFile(configDir, "auth.json", auth);
117
+ await chmod(join(configDir, "auth.json"), 0o600).catch(() => {});
116
118
  }
117
119
 
118
120
  /** Load search.json from the config directory */
119
121
  export async function loadSearchIndex(configDir: string): Promise<SearchIndex> {
120
- const raw = await readJsonFile(join(configDir, "search.json"));
121
- return raw !== undefined ? validateSearchIndex(raw) : { ...EMPTY_SEARCH_INDEX };
122
+ const raw = await readJsonFile(join(configDir, "search.json"));
123
+ return raw !== undefined ? validateSearchIndex(raw) : { ...EMPTY_SEARCH_INDEX };
122
124
  }
123
125
 
124
126
  /** Save search.json to the config directory */
125
127
  export async function saveSearchIndex(configDir: string, index: SearchIndex): Promise<void> {
126
- return saveJsonFile(configDir, "search.json", index);
128
+ return saveJsonFile(configDir, "search.json", index);
127
129
  }
128
130
 
129
131
  /** Save servers.json to the config directory */
130
132
  export async function saveServers(configDir: string, servers: ServersFile): Promise<void> {
131
- return saveJsonFile(configDir, "servers.json", servers);
133
+ return saveJsonFile(configDir, "servers.json", servers);
132
134
  }
133
135
 
134
136
  /** Load servers.json without env interpolation (preserves ${VAR} placeholders) */
135
- export async function loadRawServers(
136
- configFlag?: string,
137
- ): Promise<{ configDir: string; servers: ServersFile }> {
138
- let configDir = resolveConfigDir(configFlag);
139
-
140
- if (!(await hasServersFile(configDir))) {
141
- const cwd = process.cwd();
142
- if (await hasServersFile(cwd)) {
143
- configDir = cwd;
144
- }
145
- }
146
-
147
- const serversPath = join(configDir, "servers.json");
148
- const raw = await readJsonFile(serversPath);
149
- const servers = raw !== undefined ? validateServersFile(raw) : EMPTY_SERVERS;
150
-
151
- return { configDir, servers };
137
+ export async function loadRawServers(configFlag?: string): Promise<{ configDir: string; servers: ServersFile }> {
138
+ let configDir = resolveConfigDir(configFlag);
139
+
140
+ if (!(await hasServersFile(configDir))) {
141
+ const cwd = process.cwd();
142
+ if (await hasServersFile(cwd)) {
143
+ configDir = cwd;
144
+ }
145
+ }
146
+
147
+ const serversPath = join(configDir, "servers.json");
148
+ const raw = await readJsonFile(serversPath);
149
+ const servers = raw !== undefined ? validateServersFile(raw) : EMPTY_SERVERS;
150
+
151
+ return { configDir, servers };
152
152
  }
153
153
 
154
154
  /** Load auth.json without loading the full config */
155
155
  export async function loadRawAuth(configDir: string): Promise<AuthFile> {
156
- const authPath = join(configDir, "auth.json");
157
- const raw = await readJsonFile(authPath);
158
- return raw !== undefined ? validateAuthFile(raw) : EMPTY_AUTH;
156
+ const authPath = join(configDir, "auth.json");
157
+ const raw = await readJsonFile(authPath);
158
+ return raw !== undefined ? validateAuthFile(raw) : EMPTY_AUTH;
159
159
  }
@@ -1,64 +1,57 @@
1
- import type { Tool, Resource, Prompt } from "@modelcontextprotocol/sdk/types.js";
2
1
  import type {
3
- OAuthTokens,
4
- OAuthClientInformation,
5
- OAuthClientInformationMixed,
2
+ OAuthClientInformation,
3
+ OAuthClientInformationMixed,
4
+ OAuthTokens,
6
5
  } from "@modelcontextprotocol/sdk/shared/auth.js";
6
+ import type { Prompt, Resource, Tool } from "@modelcontextprotocol/sdk/types.js";
7
7
 
8
8
  // Re-export SDK types we use throughout the codebase
9
- export type {
10
- Tool,
11
- Resource,
12
- Prompt,
13
- OAuthTokens,
14
- OAuthClientInformation,
15
- OAuthClientInformationMixed,
16
- };
9
+ export type { OAuthClientInformation, OAuthClientInformationMixed, OAuthTokens, Prompt, Resource, Tool };
17
10
 
18
11
  // --- Server config (our format, not MCP spec) ---
19
12
 
20
13
  /** Stdio MCP server config */
21
14
  export interface StdioServerConfig {
22
- command: string;
23
- args?: string[];
24
- env?: Record<string, string>;
25
- cwd?: string;
26
- allowedTools?: string[];
27
- disabledTools?: string[];
15
+ command: string;
16
+ args?: string[];
17
+ env?: Record<string, string>;
18
+ cwd?: string;
19
+ allowedTools?: string[];
20
+ disabledTools?: string[];
28
21
  }
29
22
 
30
23
  /** HTTP MCP server config */
31
24
  export interface HttpServerConfig {
32
- url: string;
33
- headers?: Record<string, string>;
34
- transport?: "sse" | "streamable-http";
35
- allowedTools?: string[];
36
- disabledTools?: string[];
25
+ url: string;
26
+ headers?: Record<string, string>;
27
+ transport?: "sse" | "streamable-http";
28
+ allowedTools?: string[];
29
+ disabledTools?: string[];
37
30
  }
38
31
 
39
32
  export type ServerConfig = StdioServerConfig | HttpServerConfig;
40
33
 
41
34
  export function isStdioServer(config: ServerConfig): config is StdioServerConfig {
42
- return "command" in config;
35
+ return "command" in config;
43
36
  }
44
37
 
45
38
  export function isHttpServer(config: ServerConfig): config is HttpServerConfig {
46
- return "url" in config;
39
+ return "url" in config;
47
40
  }
48
41
 
49
42
  /** Top-level servers.json shape */
50
43
  export interface ServersFile {
51
- mcpServers: Record<string, ServerConfig>;
44
+ mcpServers: Record<string, ServerConfig>;
52
45
  }
53
46
 
54
47
  // --- Auth storage (wraps SDK's OAuthTokens with our persistence fields) ---
55
48
 
56
49
  /** Per-server auth entry stored in auth.json */
57
50
  export interface AuthEntry {
58
- tokens: OAuthTokens;
59
- expires_at?: string;
60
- client_info?: OAuthClientInformationMixed;
61
- complete?: boolean;
51
+ tokens: OAuthTokens;
52
+ expires_at?: string;
53
+ client_info?: OAuthClientInformationMixed;
54
+ complete?: boolean;
62
55
  }
63
56
 
64
57
  /** Top-level auth.json shape */
@@ -68,85 +61,83 @@ export type AuthFile = Record<string, AuthEntry>;
68
61
 
69
62
  /** A single tool entry in the search index */
70
63
  export interface IndexedTool {
71
- server: string;
72
- tool: string;
73
- description: string;
74
- input_schema?: Tool["inputSchema"];
75
- scenarios: string[];
76
- keywords: string[];
77
- embedding: number[];
64
+ server: string;
65
+ tool: string;
66
+ description: string;
67
+ input_schema?: Tool["inputSchema"];
68
+ scenarios: string[];
69
+ keywords: string[];
70
+ embedding: number[];
78
71
  }
79
72
 
80
73
  /** Top-level search.json shape */
81
74
  export interface SearchIndex {
82
- version: number;
83
- indexed_at: string;
84
- embedding_model: string;
85
- tools: IndexedTool[];
75
+ version: number;
76
+ indexed_at: string;
77
+ embedding_model: string;
78
+ tools: IndexedTool[];
86
79
  }
87
80
 
88
81
  // --- Combined config ---
89
82
 
90
83
  /** Validated config returned by loadConfig */
91
84
  export interface Config {
92
- configDir: string;
93
- servers: ServersFile;
94
- auth: AuthFile;
95
- searchIndex: SearchIndex;
85
+ configDir: string;
86
+ servers: ServersFile;
87
+ auth: AuthFile;
88
+ searchIndex: SearchIndex;
96
89
  }
97
90
 
98
91
  // --- Validation ---
99
92
 
100
93
  /** Validate that a parsed object looks like a valid servers.json */
101
94
  export function validateServersFile(data: unknown): ServersFile {
102
- if (typeof data !== "object" || data === null) {
103
- throw new Error("servers.json must be a JSON object");
104
- }
105
-
106
- const obj = data as Record<string, unknown>;
107
- if (typeof obj.mcpServers !== "object" || obj.mcpServers === null) {
108
- throw new Error('servers.json must have a "mcpServers" object');
109
- }
110
-
111
- const servers = obj.mcpServers as Record<string, unknown>;
112
- for (const [name, config] of Object.entries(servers)) {
113
- if (typeof config !== "object" || config === null) {
114
- throw new Error(`Server "${name}" must be an object`);
115
- }
116
- const c = config as Record<string, unknown>;
117
- const hasCommand = typeof c.command === "string";
118
- const hasUrl = typeof c.url === "string";
119
- if (!hasCommand && !hasUrl) {
120
- throw new Error(`Server "${name}" must have either "command" (stdio) or "url" (http)`);
121
- }
122
- if (hasUrl && c.transport !== undefined) {
123
- if (c.transport !== "sse" && c.transport !== "streamable-http") {
124
- throw new Error(
125
- `Server "${name}" has invalid transport "${c.transport}" — must be "sse" or "streamable-http"`,
126
- );
127
- }
128
- }
129
- }
130
-
131
- return data as ServersFile;
95
+ if (typeof data !== "object" || data === null) {
96
+ throw new Error("servers.json must be a JSON object");
97
+ }
98
+
99
+ const obj = data as Record<string, unknown>;
100
+ if (typeof obj.mcpServers !== "object" || obj.mcpServers === null) {
101
+ throw new Error('servers.json must have a "mcpServers" object');
102
+ }
103
+
104
+ const servers = obj.mcpServers as Record<string, unknown>;
105
+ for (const [name, config] of Object.entries(servers)) {
106
+ if (typeof config !== "object" || config === null) {
107
+ throw new Error(`Server "${name}" must be an object`);
108
+ }
109
+ const c = config as Record<string, unknown>;
110
+ const hasCommand = typeof c.command === "string";
111
+ const hasUrl = typeof c.url === "string";
112
+ if (!hasCommand && !hasUrl) {
113
+ throw new Error(`Server "${name}" must have either "command" (stdio) or "url" (http)`);
114
+ }
115
+ if (hasUrl && c.transport !== undefined) {
116
+ if (c.transport !== "sse" && c.transport !== "streamable-http") {
117
+ throw new Error(`Server "${name}" has invalid transport "${c.transport}" — must be "sse" or "streamable-http"`);
118
+ }
119
+ }
120
+ }
121
+
122
+ return data as ServersFile;
132
123
  }
133
124
 
134
125
  /** Validate auth.json — lenient, just check shape */
135
126
  export function validateAuthFile(data: unknown): AuthFile {
136
- if (typeof data !== "object" || data === null) {
137
- throw new Error("auth.json must be a JSON object");
138
- }
139
- return data as AuthFile;
127
+ if (typeof data !== "object" || data === null) {
128
+ throw new Error("auth.json must be a JSON object");
129
+ }
130
+ return data as AuthFile;
140
131
  }
141
132
 
142
133
  /** Validate search.json — lenient, just check shape */
143
134
  export function validateSearchIndex(data: unknown): SearchIndex {
144
- if (typeof data !== "object" || data === null) {
145
- throw new Error("search.json must be a JSON object");
146
- }
147
- const obj = data as Record<string, unknown>;
148
- if (!Array.isArray(obj.tools)) {
149
- throw new Error('search.json must have a "tools" array');
150
- }
151
- return data as SearchIndex;
135
+ if (typeof data !== "object" || data === null) {
136
+ throw new Error("search.json must be a JSON object");
137
+ }
138
+ const obj = data as Record<string, unknown>;
139
+ if (!Array.isArray(obj.tools)) {
140
+ throw new Error('search.json must have a "tools" array');
141
+ }
142
+ return data as SearchIndex;
152
143
  }
package/src/constants.ts CHANGED
@@ -1,28 +1,28 @@
1
- import { join } from "path";
2
- import { homedir } from "os";
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
3
 
4
4
  /** Default config directory (~/.mcpx) */
5
5
  export const DEFAULT_CONFIG_DIR = join(homedir(), ".mcpx");
6
6
 
7
7
  /** Environment variable names used by mcpx */
8
8
  export const ENV = {
9
- DEBUG: "MCP_DEBUG",
10
- CONCURRENCY: "MCP_CONCURRENCY",
11
- TIMEOUT: "MCP_TIMEOUT",
12
- MAX_RETRIES: "MCP_MAX_RETRIES",
13
- STRICT_ENV: "MCP_STRICT_ENV",
14
- CONFIG_PATH: "MCP_CONFIG_PATH",
15
- NO_UPDATE_CHECK: "MCPX_NO_UPDATE_CHECK",
9
+ DEBUG: "MCP_DEBUG",
10
+ CONCURRENCY: "MCP_CONCURRENCY",
11
+ TIMEOUT: "MCP_TIMEOUT",
12
+ MAX_RETRIES: "MCP_MAX_RETRIES",
13
+ STRICT_ENV: "MCP_STRICT_ENV",
14
+ CONFIG_PATH: "MCP_CONFIG_PATH",
15
+ NO_UPDATE_CHECK: "MCPX_NO_UPDATE_CHECK",
16
16
  } as const;
17
17
 
18
18
  /** Default values for configurable options */
19
19
  export const DEFAULTS = {
20
- CONCURRENCY: 5,
21
- TIMEOUT_SECONDS: 1800,
22
- MAX_RETRIES: 3,
23
- TASK_TTL_MS: 60_000,
24
- SEARCH_TOP_K: 10,
25
- LOG_LEVEL: "warning",
26
- UPDATE_CHECK_INTERVAL_MS: 24 * 60 * 60 * 1000,
27
- UPDATE_CHECK_TIMEOUT_MS: 5_000,
20
+ CONCURRENCY: 5,
21
+ TIMEOUT_SECONDS: 1800,
22
+ MAX_RETRIES: 3,
23
+ TASK_TTL_MS: 60_000,
24
+ SEARCH_TOP_K: 10,
25
+ LOG_LEVEL: "warning",
26
+ UPDATE_CHECK_INTERVAL_MS: 24 * 60 * 60 * 1000,
27
+ UPDATE_CHECK_TIMEOUT_MS: 5_000,
28
28
  } as const;
package/src/context.ts CHANGED
@@ -1,71 +1,71 @@
1
1
  import type { Command } from "commander";
2
- import { loadConfig, type LoadConfigOptions } from "./config/loader.ts";
3
2
  import { ServerManager } from "./client/manager.ts";
3
+ import { loadConfig } from "./config/loader.ts";
4
4
  import type { Config } from "./config/schemas.ts";
5
+ import { DEFAULTS, ENV } from "./constants.ts";
5
6
  import { type FormatOptions, type OutputFormat, VALID_FORMATS } from "./output/formatter.ts";
6
7
  import { logger } from "./output/logger.ts";
7
- import { ENV, DEFAULTS } from "./constants.ts";
8
8
 
9
9
  export interface AppContext {
10
- config: Config;
11
- manager: ServerManager;
12
- formatOptions: FormatOptions;
10
+ config: Config;
11
+ manager: ServerManager;
12
+ formatOptions: FormatOptions;
13
13
  }
14
14
 
15
15
  /** Build the app context from the root commander program options */
16
16
  export async function getContext(program: Command): Promise<AppContext> {
17
- const opts = program.opts();
17
+ const opts = program.opts();
18
18
 
19
- const config = await loadConfig({
20
- configFlag: opts.config as string | undefined,
21
- });
19
+ const config = await loadConfig({
20
+ configFlag: opts.config as string | undefined,
21
+ });
22
22
 
23
- const verbose = !!(
24
- (opts.verbose as boolean | undefined) ||
25
- process.env[ENV.DEBUG] === "1" ||
26
- process.env[ENV.DEBUG] === "true"
27
- );
28
- const showSecrets = !!(opts.showSecrets as boolean | undefined);
29
- const concurrency = Number(process.env[ENV.CONCURRENCY] ?? DEFAULTS.CONCURRENCY);
30
- const timeout = Number(process.env[ENV.TIMEOUT] ?? DEFAULTS.TIMEOUT_SECONDS) * 1000;
31
- const maxRetries = Number(process.env[ENV.MAX_RETRIES] ?? DEFAULTS.MAX_RETRIES);
32
- const logLevel = (opts.logLevel as string | undefined) ?? DEFAULTS.LOG_LEVEL;
23
+ const verbose = !!(
24
+ (opts.verbose as boolean | undefined) ||
25
+ process.env[ENV.DEBUG] === "1" ||
26
+ process.env[ENV.DEBUG] === "true"
27
+ );
28
+ const showSecrets = !!(opts.showSecrets as boolean | undefined);
29
+ const concurrency = Number(process.env[ENV.CONCURRENCY] ?? DEFAULTS.CONCURRENCY);
30
+ const timeout = Number(process.env[ENV.TIMEOUT] ?? DEFAULTS.TIMEOUT_SECONDS) * 1000;
31
+ const maxRetries = Number(process.env[ENV.MAX_RETRIES] ?? DEFAULTS.MAX_RETRIES);
32
+ const logLevel = (opts.logLevel as string | undefined) ?? DEFAULTS.LOG_LEVEL;
33
33
 
34
- const json = !!(opts.json as boolean | undefined);
35
- // Commander's --no-interactive sets opts.interactive = false (default true)
36
- const noInteractive = opts.interactive === false;
34
+ const json = !!(opts.json as boolean | undefined);
35
+ // Commander's --no-interactive sets opts.interactive = false (default true)
36
+ const noInteractive = opts.interactive === false;
37
37
 
38
- const formatFlag = opts.format as string | undefined;
39
- if (formatFlag && !VALID_FORMATS.includes(formatFlag as OutputFormat)) {
40
- console.error(`error: Invalid format "${formatFlag}". Use: ${VALID_FORMATS.join(", ")}`);
41
- process.exit(1);
42
- }
43
- const format = formatFlag as OutputFormat | undefined;
38
+ const formatFlag = opts.format as string | undefined;
39
+ if (formatFlag && !VALID_FORMATS.includes(formatFlag as OutputFormat)) {
40
+ console.error(`error: Invalid format "${formatFlag}". Use: ${VALID_FORMATS.join(", ")}`);
41
+ process.exit(1);
42
+ }
43
+ const format = formatFlag as OutputFormat | undefined;
44
44
 
45
- const manager = new ServerManager({
46
- servers: config.servers,
47
- configDir: config.configDir,
48
- auth: config.auth,
49
- concurrency,
50
- verbose,
51
- showSecrets,
52
- timeout,
53
- maxRetries,
54
- logLevel,
55
- json,
56
- noInteractive,
57
- });
45
+ const manager = new ServerManager({
46
+ servers: config.servers,
47
+ configDir: config.configDir,
48
+ auth: config.auth,
49
+ concurrency,
50
+ verbose,
51
+ showSecrets,
52
+ timeout,
53
+ maxRetries,
54
+ logLevel,
55
+ json,
56
+ noInteractive,
57
+ });
58
58
 
59
- const formatOptions: FormatOptions = {
60
- json: opts.json as boolean | undefined,
61
- withDescriptions: opts.withDescriptions as boolean | undefined,
62
- verbose,
63
- showSecrets,
64
- logLevel,
65
- format,
66
- };
59
+ const formatOptions: FormatOptions = {
60
+ json: opts.json as boolean | undefined,
61
+ withDescriptions: opts.withDescriptions as boolean | undefined,
62
+ verbose,
63
+ showSecrets,
64
+ logLevel,
65
+ format,
66
+ };
67
67
 
68
- logger.configure(formatOptions);
68
+ logger.configure(formatOptions);
69
69
 
70
- return { config, manager, formatOptions };
70
+ return { config, manager, formatOptions };
71
71
  }