@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,70 +1,66 @@
1
1
  import type { SearchIndex } from "../config/schemas.ts";
2
+ import { DEFAULTS } from "../constants.ts";
2
3
  import { keywordSearch } from "./keyword.ts";
3
4
  import { semanticSearch } from "./semantic.ts";
4
- import { DEFAULTS } from "../constants.ts";
5
5
 
6
6
  export interface SearchResult {
7
- server: string;
8
- tool: string;
9
- description: string;
10
- score: number;
11
- matchType: "keyword" | "semantic" | "both";
7
+ server: string;
8
+ tool: string;
9
+ description: string;
10
+ score: number;
11
+ matchType: "keyword" | "semantic" | "both";
12
12
  }
13
13
 
14
14
  export interface SearchOptions {
15
- keywordOnly?: boolean;
16
- semanticOnly?: boolean;
17
- topK?: number;
15
+ keywordOnly?: boolean;
16
+ semanticOnly?: boolean;
17
+ topK?: number;
18
18
  }
19
19
 
20
20
  /** Search tools using keyword and/or semantic matching */
21
- export async function search(
22
- query: string,
23
- index: SearchIndex,
24
- options: SearchOptions = {},
25
- ): Promise<SearchResult[]> {
26
- const topK = options.topK ?? DEFAULTS.SEARCH_TOP_K;
27
- const results = new Map<string, SearchResult>();
21
+ export async function search(query: string, index: SearchIndex, options: SearchOptions = {}): Promise<SearchResult[]> {
22
+ const topK = options.topK ?? DEFAULTS.SEARCH_TOP_K;
23
+ const results = new Map<string, SearchResult>();
28
24
 
29
- const runKeyword = !options.semanticOnly;
30
- const runSemantic = !options.keywordOnly;
25
+ const runKeyword = !options.semanticOnly;
26
+ const runSemantic = !options.keywordOnly;
31
27
 
32
- // Keyword search
33
- if (runKeyword) {
34
- const matches = keywordSearch(query, index.tools);
35
- for (const m of matches) {
36
- const key = `${m.server}/${m.tool}`;
37
- results.set(key, {
38
- server: m.server,
39
- tool: m.tool,
40
- description: m.description,
41
- score: m.score,
42
- matchType: "keyword",
43
- });
44
- }
45
- }
28
+ // Keyword search
29
+ if (runKeyword) {
30
+ const matches = keywordSearch(query, index.tools);
31
+ for (const m of matches) {
32
+ const key = `${m.server}/${m.tool}`;
33
+ results.set(key, {
34
+ server: m.server,
35
+ tool: m.tool,
36
+ description: m.description,
37
+ score: m.score,
38
+ matchType: "keyword",
39
+ });
40
+ }
41
+ }
46
42
 
47
- // Semantic search
48
- if (runSemantic && index.tools.some((t) => t.embedding.length > 0)) {
49
- const matches = await semanticSearch(query, index.tools, topK);
50
- for (const m of matches) {
51
- const key = `${m.server}/${m.tool}`;
52
- const existing = results.get(key);
53
- if (existing) {
54
- // Combine scores: keyword 0.4 + semantic 0.6
55
- existing.score = existing.score * 0.4 + m.score * 0.6;
56
- existing.matchType = "both";
57
- } else {
58
- results.set(key, {
59
- server: m.server,
60
- tool: m.tool,
61
- description: m.description,
62
- score: m.score,
63
- matchType: "semantic",
64
- });
65
- }
66
- }
67
- }
43
+ // Semantic search
44
+ if (runSemantic && index.tools.some((t) => t.embedding.length > 0)) {
45
+ const matches = await semanticSearch(query, index.tools, topK);
46
+ for (const m of matches) {
47
+ const key = `${m.server}/${m.tool}`;
48
+ const existing = results.get(key);
49
+ if (existing) {
50
+ // Combine scores: keyword 0.4 + semantic 0.6
51
+ existing.score = existing.score * 0.4 + m.score * 0.6;
52
+ existing.matchType = "both";
53
+ } else {
54
+ results.set(key, {
55
+ server: m.server,
56
+ tool: m.tool,
57
+ description: m.description,
58
+ score: m.score,
59
+ matchType: "semantic",
60
+ });
61
+ }
62
+ }
63
+ }
68
64
 
69
- return [...results.values()].sort((a, b) => b.score - a.score).slice(0, topK);
65
+ return [...results.values()].sort((a, b) => b.score - a.score).slice(0, topK);
70
66
  }
@@ -1,92 +1,92 @@
1
1
  import type { ServerManager, ToolWithServer } from "../client/manager.ts";
2
- import type { SearchIndex, IndexedTool } from "../config/schemas.ts";
3
- import { generateEmbedding } from "./semantic.ts";
2
+ import type { IndexedTool, SearchIndex } from "../config/schemas.ts";
4
3
  import { logger } from "../output/logger.ts";
4
+ import { generateEmbedding } from "./semantic.ts";
5
5
 
6
6
  /** Extract keywords from a tool name by splitting on separators and camelCase */
7
7
  export function extractKeywords(name: string): string[] {
8
- // Split on underscores, hyphens, dots
9
- const parts = name.split(/[_\-.]+/);
8
+ // Split on underscores, hyphens, dots
9
+ const parts = name.split(/[_\-.]+/);
10
10
 
11
- // Also split camelCase
12
- const words: string[] = [];
13
- for (const part of parts) {
14
- words.push(...part.replace(/([a-z])([A-Z])/g, "$1 $2").split(/\s+/));
15
- }
11
+ // Also split camelCase
12
+ const words: string[] = [];
13
+ for (const part of parts) {
14
+ words.push(...part.replace(/([a-z])([A-Z])/g, "$1 $2").split(/\s+/));
15
+ }
16
16
 
17
- return words.map((w) => w.toLowerCase()).filter((w) => w.length > 1);
17
+ return words.map((w) => w.toLowerCase()).filter((w) => w.length > 1);
18
18
  }
19
19
 
20
20
  /** Generate scenario phrases from tool name and description */
21
21
  export function generateScenarios(name: string, description: string): string[] {
22
- const scenarios: string[] = [];
22
+ const scenarios: string[] = [];
23
23
 
24
- // Use description as-is if short enough
25
- if (description && description.length < 200) {
26
- scenarios.push(description);
27
- }
24
+ // Use description as-is if short enough
25
+ if (description && description.length < 200) {
26
+ scenarios.push(description);
27
+ }
28
28
 
29
- // Extract action + noun from tool name (e.g., "SendMessage" → "send a message")
30
- const keywords = extractKeywords(name);
31
- if (keywords.length >= 2) {
32
- scenarios.push(keywords.join(" "));
33
- }
29
+ // Extract action + noun from tool name (e.g., "SendMessage" → "send a message")
30
+ const keywords = extractKeywords(name);
31
+ if (keywords.length >= 2) {
32
+ scenarios.push(keywords.join(" "));
33
+ }
34
34
 
35
- return scenarios;
35
+ return scenarios;
36
36
  }
37
37
 
38
38
  /** Build an IndexedTool from a tool with server info */
39
39
  async function indexTool(t: ToolWithServer): Promise<IndexedTool> {
40
- const description = t.tool.description ?? "";
41
- const keywords = extractKeywords(t.tool.name);
42
- const scenarios = generateScenarios(t.tool.name, description);
43
-
44
- // Build text for embedding: combine name, description, and scenarios
45
- const embeddingText = [t.tool.name, description, ...scenarios].filter(Boolean).join(" ");
46
- const embedding = await generateEmbedding(embeddingText);
47
-
48
- return {
49
- server: t.server,
50
- tool: t.tool.name,
51
- description,
52
- input_schema: t.tool.inputSchema,
53
- scenarios,
54
- keywords,
55
- embedding,
56
- };
40
+ const description = t.tool.description ?? "";
41
+ const keywords = extractKeywords(t.tool.name);
42
+ const scenarios = generateScenarios(t.tool.name, description);
43
+
44
+ // Build text for embedding: combine name, description, and scenarios
45
+ const embeddingText = [t.tool.name, description, ...scenarios].filter(Boolean).join(" ");
46
+ const embedding = await generateEmbedding(embeddingText);
47
+
48
+ return {
49
+ server: t.server,
50
+ tool: t.tool.name,
51
+ description,
52
+ input_schema: t.tool.inputSchema,
53
+ scenarios,
54
+ keywords,
55
+ embedding,
56
+ };
57
57
  }
58
58
 
59
59
  export interface IndexProgress {
60
- total: number;
61
- current: number;
62
- tool: string;
60
+ total: number;
61
+ current: number;
62
+ tool: string;
63
63
  }
64
64
 
65
65
  /** Build a search index from all configured servers */
66
66
  export async function buildSearchIndex(
67
- manager: ServerManager,
68
- onProgress?: (progress: IndexProgress) => void,
67
+ manager: ServerManager,
68
+ onProgress?: (progress: IndexProgress) => void,
69
69
  ): Promise<SearchIndex> {
70
- const { tools, errors } = await manager.getAllTools();
71
-
72
- if (errors.length > 0) {
73
- for (const err of errors) {
74
- logger.warn(`${err.server}: ${err.message}`);
75
- }
76
- }
77
-
78
- const indexed: IndexedTool[] = [];
79
-
80
- for (let i = 0; i < tools.length; i++) {
81
- const t = tools[i]!;
82
- onProgress?.({ total: tools.length, current: i + 1, tool: `${t.server}/${t.tool.name}` });
83
- indexed.push(await indexTool(t));
84
- }
85
-
86
- return {
87
- version: 1,
88
- indexed_at: new Date().toISOString(),
89
- embedding_model: "Xenova/all-MiniLM-L6-v2",
90
- tools: indexed,
91
- };
70
+ const { tools, errors } = await manager.getAllTools();
71
+
72
+ if (errors.length > 0) {
73
+ for (const err of errors) {
74
+ logger.warn(`${err.server}: ${err.message}`);
75
+ }
76
+ }
77
+
78
+ const indexed: IndexedTool[] = [];
79
+
80
+ for (let i = 0; i < tools.length; i++) {
81
+ const t = tools[i]!;
82
+ onProgress?.({ total: tools.length, current: i + 1, tool: `${t.server}/${t.tool.name}` });
83
+ indexed.push(await indexTool(t));
84
+ }
85
+
86
+ return {
87
+ version: 1,
88
+ indexed_at: new Date().toISOString(),
89
+ embedding_model: "Xenova/all-MiniLM-L6-v2",
90
+ tools: indexed,
91
+ };
92
92
  }
@@ -3,81 +3,81 @@ import type { IndexedTool } from "../config/schemas.ts";
3
3
  import type { BaseMatch } from "./types.ts";
4
4
 
5
5
  export interface KeywordMatch extends BaseMatch {
6
- matchedField: string;
6
+ matchedField: string;
7
7
  }
8
8
 
9
9
  interface FieldWeight {
10
- field: string;
11
- weight: number;
12
- values: (t: IndexedTool) => string[];
10
+ field: string;
11
+ weight: number;
12
+ values: (t: IndexedTool) => string[];
13
13
  }
14
14
 
15
15
  const FIELDS: FieldWeight[] = [
16
- { field: "name", weight: 1.0, values: (t) => [t.tool] },
17
- { field: "keyword", weight: 0.8, values: (t) => t.keywords },
18
- { field: "scenario", weight: 0.6, values: (t) => t.scenarios },
19
- { field: "description", weight: 0.4, values: (t) => [t.description] },
16
+ { field: "name", weight: 1.0, values: (t) => [t.tool] },
17
+ { field: "keyword", weight: 0.8, values: (t) => t.keywords },
18
+ { field: "scenario", weight: 0.6, values: (t) => t.scenarios },
19
+ { field: "description", weight: 0.4, values: (t) => [t.description] },
20
20
  ];
21
21
 
22
22
  /** Check if query looks like a glob pattern */
23
23
  function isGlob(query: string): boolean {
24
- return /[*?[\]{}]/.test(query);
24
+ return /[*?[\]{}]/.test(query);
25
25
  }
26
26
 
27
27
  /** Search indexed tools by keyword/glob matching */
28
28
  export function keywordSearch(query: string, tools: IndexedTool[]): KeywordMatch[] {
29
- const queryLower = query.toLowerCase();
30
- const tokens = queryLower.split(/\s+/).filter(Boolean);
29
+ const queryLower = query.toLowerCase();
30
+ const tokens = queryLower.split(/\s+/).filter(Boolean);
31
31
 
32
- // If any token is a glob, use picomatch for name matching
33
- const globTokens = tokens.filter(isGlob);
34
- const textTokens = tokens.filter((t) => !isGlob(t));
35
- const globMatcher = globTokens.length > 0 ? picomatch(globTokens, { nocase: true }) : null;
32
+ // If any token is a glob, use picomatch for name matching
33
+ const globTokens = tokens.filter(isGlob);
34
+ const textTokens = tokens.filter((t) => !isGlob(t));
35
+ const globMatcher = globTokens.length > 0 ? picomatch(globTokens, { nocase: true }) : null;
36
36
 
37
- const results: KeywordMatch[] = [];
37
+ const results: KeywordMatch[] = [];
38
38
 
39
- for (const tool of tools) {
40
- let bestScore = 0;
41
- let bestField = "";
39
+ for (const tool of tools) {
40
+ let bestScore = 0;
41
+ let bestField = "";
42
42
 
43
- // Glob matching against tool name
44
- if (globMatcher && globMatcher(tool.tool)) {
45
- bestScore = 1.0;
46
- bestField = "name";
47
- }
43
+ // Glob matching against tool name
44
+ if (globMatcher?.(tool.tool)) {
45
+ bestScore = 1.0;
46
+ bestField = "name";
47
+ }
48
48
 
49
- // Text token matching against all fields
50
- if (textTokens.length > 0) {
51
- for (const { field, weight, values } of FIELDS) {
52
- const fieldValues = values(tool).map((v) => v.toLowerCase());
53
- let matchCount = 0;
49
+ // Text token matching against all fields
50
+ if (textTokens.length > 0) {
51
+ for (const { field, weight, values } of FIELDS) {
52
+ const fieldValues = values(tool).map((v) => v.toLowerCase());
53
+ let matchCount = 0;
54
54
 
55
- for (const token of textTokens) {
56
- if (fieldValues.some((v) => v.includes(token))) {
57
- matchCount++;
58
- }
59
- }
55
+ for (const token of textTokens) {
56
+ if (fieldValues.some((v) => v.includes(token))) {
57
+ matchCount++;
58
+ }
59
+ }
60
60
 
61
- if (matchCount > 0) {
62
- const score = (matchCount / textTokens.length) * weight;
63
- if (score > bestScore) {
64
- bestScore = score;
65
- bestField = field;
66
- }
67
- }
68
- }
69
- }
61
+ if (matchCount > 0) {
62
+ const score = (matchCount / textTokens.length) * weight;
63
+ if (score > bestScore) {
64
+ bestScore = score;
65
+ bestField = field;
66
+ }
67
+ }
68
+ }
69
+ }
70
70
 
71
- if (bestScore > 0) {
72
- results.push({
73
- server: tool.server,
74
- tool: tool.tool,
75
- description: tool.description,
76
- score: bestScore,
77
- matchedField: bestField,
78
- });
79
- }
80
- }
71
+ if (bestScore > 0) {
72
+ results.push({
73
+ server: tool.server,
74
+ tool: tool.tool,
75
+ description: tool.description,
76
+ score: bestScore,
77
+ matchedField: bestField,
78
+ });
79
+ }
80
+ }
81
81
 
82
- return results.sort((a, b) => b.score - a.score);
82
+ return results.sort((a, b) => b.score - a.score);
83
83
  }
@@ -9,64 +9,64 @@ let pipelineInstance: ((text: string) => Promise<Float32Array>) | null = null;
9
9
 
10
10
  /** Get or create the embedding pipeline */
11
11
  async function getEmbedder(): Promise<(text: string) => Promise<Float32Array>> {
12
- if (pipelineInstance) return pipelineInstance;
12
+ if (pipelineInstance) return pipelineInstance;
13
13
 
14
- const { pipeline } = await import("@huggingface/transformers");
15
- const extractor = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2", {
16
- dtype: "fp32",
17
- });
14
+ const { pipeline } = await import("@huggingface/transformers");
15
+ const extractor = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2", {
16
+ dtype: "fp32",
17
+ });
18
18
 
19
- pipelineInstance = async (text: string): Promise<Float32Array> => {
20
- const output = await extractor(text, { pooling: "mean", normalize: true });
21
- // output.data is a Float32Array of the pooled embedding
22
- return output.data as Float32Array;
23
- };
19
+ pipelineInstance = async (text: string): Promise<Float32Array> => {
20
+ const output = await extractor(text, { pooling: "mean", normalize: true });
21
+ // output.data is a Float32Array of the pooled embedding
22
+ return output.data as Float32Array;
23
+ };
24
24
 
25
- return pipelineInstance;
25
+ return pipelineInstance;
26
26
  }
27
27
 
28
28
  /** Generate an embedding vector for text */
29
29
  export async function generateEmbedding(text: string): Promise<number[]> {
30
- const embed = await getEmbedder();
31
- const vec = await embed(text);
32
- return Array.from(vec);
30
+ const embed = await getEmbedder();
31
+ const vec = await embed(text);
32
+ return Array.from(vec);
33
33
  }
34
34
 
35
35
  /** Cosine similarity between two vectors */
36
36
  export function cosineSimilarity(a: number[], b: number[]): number {
37
- if (a.length !== b.length || a.length === 0) return 0;
37
+ if (a.length !== b.length || a.length === 0) return 0;
38
38
 
39
- let dot = 0;
40
- let magA = 0;
41
- let magB = 0;
42
- for (let i = 0; i < a.length; i++) {
43
- dot += a[i]! * b[i]!;
44
- magA += a[i]! * a[i]!;
45
- magB += b[i]! * b[i]!;
46
- }
39
+ let dot = 0;
40
+ let magA = 0;
41
+ let magB = 0;
42
+ for (let i = 0; i < a.length; i++) {
43
+ dot += a[i]! * b[i]!;
44
+ magA += a[i]! * a[i]!;
45
+ magB += b[i]! * b[i]!;
46
+ }
47
47
 
48
- const denom = Math.sqrt(magA) * Math.sqrt(magB);
49
- return denom === 0 ? 0 : dot / denom;
48
+ const denom = Math.sqrt(magA) * Math.sqrt(magB);
49
+ return denom === 0 ? 0 : dot / denom;
50
50
  }
51
51
 
52
52
  /** Search indexed tools by semantic similarity */
53
53
  export async function semanticSearch(
54
- query: string,
55
- tools: IndexedTool[],
56
- topK: number = DEFAULTS.SEARCH_TOP_K,
54
+ query: string,
55
+ tools: IndexedTool[],
56
+ topK: number = DEFAULTS.SEARCH_TOP_K,
57
57
  ): Promise<SemanticMatch[]> {
58
- // Only search tools that have embeddings
59
- const withEmbeddings = tools.filter((t) => t.embedding.length > 0);
60
- if (withEmbeddings.length === 0) return [];
58
+ // Only search tools that have embeddings
59
+ const withEmbeddings = tools.filter((t) => t.embedding.length > 0);
60
+ if (withEmbeddings.length === 0) return [];
61
61
 
62
- const queryEmbedding = await generateEmbedding(query);
62
+ const queryEmbedding = await generateEmbedding(query);
63
63
 
64
- const scored = withEmbeddings.map((tool) => ({
65
- server: tool.server,
66
- tool: tool.tool,
67
- description: tool.description,
68
- score: cosineSimilarity(queryEmbedding, tool.embedding),
69
- }));
64
+ const scored = withEmbeddings.map((tool) => ({
65
+ server: tool.server,
66
+ tool: tool.tool,
67
+ description: tool.description,
68
+ score: cosineSimilarity(queryEmbedding, tool.embedding),
69
+ }));
70
70
 
71
- return scored.sort((a, b) => b.score - a.score).slice(0, topK);
71
+ return scored.sort((a, b) => b.score - a.score).slice(0, topK);
72
72
  }
@@ -2,7 +2,7 @@ import type { SearchIndex, ServersFile } from "../config/schemas.ts";
2
2
 
3
3
  /** Return server names that appear in the index but not in the current config */
4
4
  export function getStaleServers(index: SearchIndex, servers: ServersFile): string[] {
5
- const configured = new Set(Object.keys(servers.mcpServers));
6
- const indexed = new Set(index.tools.map((t) => t.server));
7
- return [...indexed].filter((s) => !configured.has(s));
5
+ const configured = new Set(Object.keys(servers.mcpServers));
6
+ const indexed = new Set(index.tools.map((t) => t.server));
7
+ return [...indexed].filter((s) => !configured.has(s));
8
8
  }
@@ -1,7 +1,7 @@
1
1
  /** Base interface for tool search matches */
2
2
  export interface BaseMatch {
3
- server: string;
4
- tool: string;
5
- description: string;
6
- score: number;
3
+ server: string;
4
+ tool: string;
5
+ description: string;
6
+ score: number;
7
7
  }