@aiwerk/mcp-bridge 1.2.3 → 1.4.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
@@ -115,10 +115,38 @@ Config: `~/.mcp-bridge/config.json` | Secrets: `~/.mcp-bridge/.env`
115
115
  },
116
116
  "toolPrefix": true,
117
117
  "connectionTimeoutMs": 5000,
118
- "requestTimeoutMs": 60000
118
+ "requestTimeoutMs": 60000,
119
+ "schemaCompression": {
120
+ "enabled": true,
121
+ "maxDescriptionLength": 80
122
+ }
119
123
  }
120
124
  ```
121
125
 
126
+ ### Schema Compression
127
+
128
+ In router mode, tool descriptions from upstream servers can be verbose (100-300+ chars each). Schema compression truncates them to save tokens:
129
+
130
+ - **Enabled by default** — descriptions capped at 80 characters
131
+ - Cuts at sentence boundary when possible, otherwise word boundary
132
+ - Use `action=schema` to retrieve the full uncompressed schema for any tool on demand
133
+
134
+ ```json
135
+ "schemaCompression": {
136
+ "enabled": true,
137
+ "maxDescriptionLength": 80
138
+ }
139
+ ```
140
+
141
+ **Token savings example:** 30 Todoist tools: ~2800 tokens uncompressed -> ~1200 compressed (~57% reduction).
142
+
143
+ To get full details for a specific tool:
144
+ ```
145
+ mcp(server="todoist", action="schema", tool="find-tasks")
146
+ ```
147
+
148
+ Set `"enabled": false` to disable compression and return full descriptions.
149
+
122
150
  ### Modes
123
151
 
124
152
  | Mode | Tools exposed | Best for |
@@ -186,6 +214,9 @@ Built-in catalog with pre-configured servers:
186
214
  | wise | stdio | International payments |
187
215
  | tavily | stdio | AI-optimized web search |
188
216
  | apify | streamable-http | Web scraping and automation |
217
+ | atlassian | stdio | Confluence and Jira |
218
+ | chrome-devtools | stdio | Chrome browser automation |
219
+ | hostinger | sse | Web hosting management |
189
220
 
190
221
  ```bash
191
222
  mcp-bridge install todoist # Interactive setup with API key prompt
@@ -0,0 +1,37 @@
1
+ import type { Logger } from "./types.js";
2
+ export interface EmbeddingProvider {
3
+ embed(texts: string[]): Promise<number[][]>;
4
+ dimensions(): number;
5
+ }
6
+ export declare class GeminiEmbedding implements EmbeddingProvider {
7
+ private readonly apiKey;
8
+ constructor(apiKey: string);
9
+ embed(texts: string[]): Promise<number[][]>;
10
+ dimensions(): number;
11
+ }
12
+ export declare class OpenAIEmbedding implements EmbeddingProvider {
13
+ private readonly apiKey;
14
+ private readonly model;
15
+ constructor(apiKey: string, model?: string);
16
+ embed(texts: string[]): Promise<number[][]>;
17
+ dimensions(): number;
18
+ }
19
+ export declare class OllamaEmbedding implements EmbeddingProvider {
20
+ private readonly model;
21
+ private readonly baseUrl;
22
+ constructor(model?: string, baseUrl?: string);
23
+ embed(texts: string[]): Promise<number[][]>;
24
+ dimensions(): number;
25
+ }
26
+ export declare class KeywordEmbedding implements EmbeddingProvider {
27
+ private vocabulary;
28
+ private vocabSize;
29
+ embed(texts: string[]): Promise<number[][]>;
30
+ dimensions(): number;
31
+ private tokenize;
32
+ }
33
+ export declare function createEmbeddingProvider(type: "auto" | "gemini" | "openai" | "ollama" | "keyword", config?: {
34
+ model?: string;
35
+ apiKey?: string;
36
+ ollamaUrl?: string;
37
+ }, logger?: Logger): EmbeddingProvider;
@@ -0,0 +1,176 @@
1
+ export class GeminiEmbedding {
2
+ apiKey;
3
+ constructor(apiKey) {
4
+ this.apiKey = apiKey;
5
+ }
6
+ async embed(texts) {
7
+ const results = [];
8
+ for (const text of texts) {
9
+ const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent?key=${this.apiKey}`, {
10
+ method: "POST",
11
+ headers: { "Content-Type": "application/json" },
12
+ body: JSON.stringify({
13
+ content: { parts: [{ text }] }
14
+ })
15
+ });
16
+ if (!response.ok) {
17
+ throw new Error(`Gemini API error: ${response.status} ${response.statusText}`);
18
+ }
19
+ const data = await response.json();
20
+ results.push(data.embedding.values);
21
+ }
22
+ return results;
23
+ }
24
+ dimensions() {
25
+ return 768;
26
+ }
27
+ }
28
+ export class OpenAIEmbedding {
29
+ apiKey;
30
+ model;
31
+ constructor(apiKey, model = "text-embedding-3-small") {
32
+ this.apiKey = apiKey;
33
+ this.model = model;
34
+ }
35
+ async embed(texts) {
36
+ const response = await fetch("https://api.openai.com/v1/embeddings", {
37
+ method: "POST",
38
+ headers: {
39
+ "Content-Type": "application/json",
40
+ Authorization: `Bearer ${this.apiKey}`
41
+ },
42
+ body: JSON.stringify({ input: texts, model: this.model })
43
+ });
44
+ if (!response.ok) {
45
+ throw new Error(`OpenAI API error: ${response.status} ${response.statusText}`);
46
+ }
47
+ const data = await response.json();
48
+ return data.data
49
+ .sort((a, b) => a.index - b.index)
50
+ .map((item) => item.embedding);
51
+ }
52
+ dimensions() {
53
+ return 1536;
54
+ }
55
+ }
56
+ export class OllamaEmbedding {
57
+ model;
58
+ baseUrl;
59
+ constructor(model = "nomic-embed-text", baseUrl = "http://localhost:11434") {
60
+ this.model = model;
61
+ this.baseUrl = baseUrl;
62
+ }
63
+ async embed(texts) {
64
+ const response = await fetch(`${this.baseUrl}/api/embed`, {
65
+ method: "POST",
66
+ headers: { "Content-Type": "application/json" },
67
+ body: JSON.stringify({ model: this.model, input: texts })
68
+ });
69
+ if (!response.ok) {
70
+ throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
71
+ }
72
+ const data = await response.json();
73
+ return data.embeddings;
74
+ }
75
+ dimensions() {
76
+ return 768;
77
+ }
78
+ }
79
+ export class KeywordEmbedding {
80
+ vocabulary = new Map();
81
+ vocabSize = 0;
82
+ async embed(texts) {
83
+ // Build vocabulary from all texts
84
+ this.vocabulary.clear();
85
+ this.vocabSize = 0;
86
+ const tokenizedTexts = texts.map((text) => this.tokenize(text));
87
+ for (const tokens of tokenizedTexts) {
88
+ for (const token of tokens) {
89
+ if (!this.vocabulary.has(token)) {
90
+ this.vocabulary.set(token, this.vocabSize++);
91
+ }
92
+ }
93
+ }
94
+ if (this.vocabSize === 0) {
95
+ return texts.map(() => [0]);
96
+ }
97
+ // Create TF vectors
98
+ const vectors = [];
99
+ for (const tokens of tokenizedTexts) {
100
+ const vector = new Array(this.vocabSize).fill(0);
101
+ for (const token of tokens) {
102
+ const idx = this.vocabulary.get(token);
103
+ vector[idx] += 1;
104
+ }
105
+ // Normalize by document length
106
+ const len = tokens.length || 1;
107
+ for (let i = 0; i < vector.length; i++) {
108
+ vector[i] /= len;
109
+ }
110
+ vectors.push(vector);
111
+ }
112
+ // Apply IDF weighting
113
+ const docCount = texts.length;
114
+ const idf = new Array(this.vocabSize).fill(0);
115
+ for (let i = 0; i < this.vocabSize; i++) {
116
+ let df = 0;
117
+ for (const vec of vectors) {
118
+ if (vec[i] > 0)
119
+ df++;
120
+ }
121
+ idf[i] = Math.log((docCount + 1) / (df + 1)) + 1;
122
+ }
123
+ for (const vec of vectors) {
124
+ for (let i = 0; i < vec.length; i++) {
125
+ vec[i] *= idf[i];
126
+ }
127
+ }
128
+ return vectors;
129
+ }
130
+ dimensions() {
131
+ return this.vocabSize || 1;
132
+ }
133
+ tokenize(text) {
134
+ return text
135
+ .toLowerCase()
136
+ .replace(/[^a-z0-9\s]/g, " ")
137
+ .split(/\s+/)
138
+ .filter((t) => t.length > 1);
139
+ }
140
+ }
141
+ export function createEmbeddingProvider(type, config, logger) {
142
+ if (type === "gemini") {
143
+ const key = config?.apiKey || process.env.GEMINI_API_KEY;
144
+ if (!key)
145
+ throw new Error("GEMINI_API_KEY is required for gemini embedding provider");
146
+ return new GeminiEmbedding(key);
147
+ }
148
+ if (type === "openai") {
149
+ const key = config?.apiKey || process.env.OPENAI_API_KEY;
150
+ if (!key)
151
+ throw new Error("OPENAI_API_KEY is required for openai embedding provider");
152
+ return new OpenAIEmbedding(key, config?.model);
153
+ }
154
+ if (type === "ollama") {
155
+ return new OllamaEmbedding(config?.model, config?.ollamaUrl);
156
+ }
157
+ if (type === "keyword") {
158
+ return new KeywordEmbedding();
159
+ }
160
+ // auto: try gemini, openai, ollama, then keyword fallback
161
+ const geminiKey = config?.apiKey || process.env.GEMINI_API_KEY;
162
+ if (geminiKey) {
163
+ logger?.debug("[mcp-bridge] Intent routing: using Gemini embeddings");
164
+ return new GeminiEmbedding(geminiKey);
165
+ }
166
+ const openaiKey = process.env.OPENAI_API_KEY;
167
+ if (openaiKey) {
168
+ logger?.debug("[mcp-bridge] Intent routing: using OpenAI embeddings");
169
+ return new OpenAIEmbedding(openaiKey, config?.model);
170
+ }
171
+ // For ollama, we can't synchronously check reachability in factory,
172
+ // so we skip it in auto mode and fall back to keyword.
173
+ // Users who want ollama should specify type="ollama" explicitly.
174
+ logger?.debug("[mcp-bridge] Intent routing: using keyword fallback embeddings");
175
+ return new KeywordEmbedding();
176
+ }
@@ -0,0 +1,25 @@
1
+ import type { EmbeddingProvider } from "./embeddings.js";
2
+ import type { Logger } from "./types.js";
3
+ import type { McpTool } from "./types.js";
4
+ export interface IntentMatch {
5
+ server: string;
6
+ tool: string;
7
+ score: number;
8
+ alternatives: Array<{
9
+ server: string;
10
+ tool: string;
11
+ score: number;
12
+ }>;
13
+ }
14
+ export declare class IntentRouter {
15
+ private readonly embeddingProvider;
16
+ private readonly logger;
17
+ private readonly store;
18
+ private indexed;
19
+ private minScore;
20
+ constructor(embeddingProvider: EmbeddingProvider, logger: Logger, minScore?: number);
21
+ indexTools(servers: Record<string, McpTool[]>): Promise<void>;
22
+ resolve(intent: string): Promise<IntentMatch | null>;
23
+ isIndexed(): boolean;
24
+ clearIndex(): void;
25
+ }
@@ -0,0 +1,80 @@
1
+ import { VectorStore } from "./vector-store.js";
2
+ const DEFAULT_MIN_SCORE = 0.3;
3
+ const DEFAULT_TOP_K = 4;
4
+ export class IntentRouter {
5
+ embeddingProvider;
6
+ logger;
7
+ store = new VectorStore();
8
+ indexed = false;
9
+ minScore;
10
+ constructor(embeddingProvider, logger, minScore) {
11
+ this.embeddingProvider = embeddingProvider;
12
+ this.logger = logger;
13
+ this.minScore = minScore ?? DEFAULT_MIN_SCORE;
14
+ }
15
+ async indexTools(servers) {
16
+ this.store.clear();
17
+ this.indexed = false;
18
+ const entries = [];
19
+ for (const [serverName, tools] of Object.entries(servers)) {
20
+ for (const tool of tools) {
21
+ const text = `${tool.name}: ${tool.description || ""}`;
22
+ entries.push({
23
+ id: `${serverName}/${tool.name}`,
24
+ text,
25
+ server: serverName,
26
+ tool: tool.name
27
+ });
28
+ }
29
+ }
30
+ if (entries.length === 0) {
31
+ this.indexed = true;
32
+ return;
33
+ }
34
+ const texts = entries.map((e) => e.text);
35
+ const vectors = await this.embeddingProvider.embed(texts);
36
+ for (let i = 0; i < entries.length; i++) {
37
+ this.store.add(entries[i].id, vectors[i], {
38
+ server: entries[i].server,
39
+ tool: entries[i].tool,
40
+ description: entries[i].text
41
+ });
42
+ }
43
+ this.indexed = true;
44
+ this.logger.debug(`[mcp-bridge] Intent router indexed ${entries.length} tools`);
45
+ }
46
+ async resolve(intent) {
47
+ if (!this.indexed || this.store.size() === 0) {
48
+ return null;
49
+ }
50
+ // Embed the intent together with stored texts for keyword provider compatibility
51
+ // For API-based providers this is just a single text embedding
52
+ const [queryVector] = await this.embeddingProvider.embed([intent]);
53
+ const results = this.store.search(queryVector, DEFAULT_TOP_K);
54
+ if (results.length === 0) {
55
+ return null;
56
+ }
57
+ const best = results[0];
58
+ if (best.score < this.minScore) {
59
+ this.logger.debug(`[mcp-bridge] Intent "${intent}" best match score ${best.score.toFixed(3)} below threshold ${this.minScore}`);
60
+ return null;
61
+ }
62
+ return {
63
+ server: best.metadata.server,
64
+ tool: best.metadata.tool,
65
+ score: best.score,
66
+ alternatives: results.slice(1).map((r) => ({
67
+ server: r.metadata.server,
68
+ tool: r.metadata.tool,
69
+ score: r.score
70
+ }))
71
+ };
72
+ }
73
+ isIndexed() {
74
+ return this.indexed;
75
+ }
76
+ clearIndex() {
77
+ this.store.clear();
78
+ this.indexed = false;
79
+ }
80
+ }
@@ -26,9 +26,28 @@ export type RouterDispatchResponse = {
26
26
  action: "call";
27
27
  tool: string;
28
28
  result: any;
29
+ } | {
30
+ server: string;
31
+ action: "schema";
32
+ tool: string;
33
+ schema: any;
34
+ description: string;
29
35
  } | {
30
36
  action: "status";
31
37
  servers: RouterServerStatus[];
38
+ } | {
39
+ action: "intent";
40
+ intent: string;
41
+ match: {
42
+ server: string;
43
+ tool: string;
44
+ score: number;
45
+ };
46
+ alternatives: Array<{
47
+ server: string;
48
+ tool: string;
49
+ score: number;
50
+ }>;
32
51
  } | {
33
52
  error: RouterErrorCode;
34
53
  message: string;
@@ -48,10 +67,12 @@ export declare class McpRouter {
48
67
  private readonly idleTimeoutMs;
49
68
  private readonly maxConcurrent;
50
69
  private readonly states;
70
+ private intentRouter;
51
71
  constructor(servers: Record<string, McpServerConfig>, clientConfig: McpClientConfig, logger: Logger, transportRefs?: Partial<RouterTransportRefs>);
52
72
  static generateDescription(servers: Record<string, McpServerConfig>): string;
53
73
  dispatch(server?: string, action?: string, tool?: string, params?: any): Promise<RouterDispatchResponse>;
54
74
  getToolList(server: string): Promise<RouterToolHint[]>;
75
+ private resolveIntent;
55
76
  private getStatus;
56
77
  disconnectAll(): Promise<void>;
57
78
  private ensureConnected;
@@ -2,6 +2,9 @@ import { SseTransport } from "./transport-sse.js";
2
2
  import { StdioTransport } from "./transport-stdio.js";
3
3
  import { StreamableHttpTransport } from "./transport-streamable-http.js";
4
4
  import { fetchToolsList, initializeProtocol, PACKAGE_VERSION } from "./protocol.js";
5
+ import { compressDescription } from "./schema-compression.js";
6
+ import { IntentRouter } from "./intent-router.js";
7
+ import { createEmbeddingProvider } from "./embeddings.js";
5
8
  const DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
6
9
  const DEFAULT_MAX_CONCURRENT = 5;
7
10
  export class McpRouter {
@@ -12,6 +15,7 @@ export class McpRouter {
12
15
  idleTimeoutMs;
13
16
  maxConcurrent;
14
17
  states = new Map();
18
+ intentRouter = null;
15
19
  constructor(servers, clientConfig, logger, transportRefs) {
16
20
  this.servers = servers;
17
21
  this.clientConfig = clientConfig;
@@ -44,6 +48,14 @@ export class McpRouter {
44
48
  if (normalizedAction === "status") {
45
49
  return this.getStatus();
46
50
  }
51
+ // Intent action: find server+tool from natural language
52
+ if (normalizedAction === "intent") {
53
+ const intent = params?.intent || tool;
54
+ if (!intent) {
55
+ return this.error("invalid_params", "intent string is required for action=intent");
56
+ }
57
+ return this.resolveIntent(intent);
58
+ }
47
59
  if (!server) {
48
60
  return this.error("invalid_params", "server is required");
49
61
  }
@@ -59,11 +71,33 @@ export class McpRouter {
59
71
  return this.error("connection_failed", `Failed to connect to ${server}: ${error instanceof Error ? error.message : String(error)}`);
60
72
  }
61
73
  }
74
+ if (normalizedAction === "schema") {
75
+ if (!tool) {
76
+ return this.error("invalid_params", "tool is required for action=schema");
77
+ }
78
+ try {
79
+ await this.getToolList(server);
80
+ }
81
+ catch (error) {
82
+ return this.error("connection_failed", `Failed to connect to ${server}: ${error instanceof Error ? error.message : String(error)}`);
83
+ }
84
+ const state = this.states.get(server);
85
+ const fullTool = state.fullToolsMap?.get(tool);
86
+ if (!fullTool) {
87
+ return this.error("unknown_tool", `Tool '${tool}' not found on server '${server}'`, state.toolNames);
88
+ }
89
+ return { server, action: "schema", tool, schema: fullTool.inputSchema, description: fullTool.description };
90
+ }
62
91
  if (normalizedAction === "refresh") {
63
92
  try {
64
93
  const state = await this.ensureConnected(server);
65
94
  state.toolsCache = undefined;
95
+ state.fullToolsMap = undefined;
66
96
  state.toolNames = [];
97
+ // Clear intent index so it re-indexes on next intent query
98
+ if (this.intentRouter) {
99
+ this.intentRouter.clearIndex();
100
+ }
67
101
  const tools = await this.getToolList(server);
68
102
  return { server, action: "refresh", refreshed: true, tools };
69
103
  }
@@ -72,7 +106,7 @@ export class McpRouter {
72
106
  }
73
107
  }
74
108
  if (normalizedAction !== "call") {
75
- return this.error("invalid_params", `action must be one of: list, call, refresh`);
109
+ return this.error("invalid_params", `action must be one of: list, call, refresh, schema, intent`);
76
110
  }
77
111
  if (!tool) {
78
112
  return this.error("invalid_params", "tool is required for action=call");
@@ -116,14 +150,65 @@ export class McpRouter {
116
150
  }
117
151
  const tools = await fetchToolsList(state.transport);
118
152
  state.toolNames = tools.map((tool) => tool.name);
153
+ // Store full tool metadata for action=schema
154
+ state.fullToolsMap = new Map(tools.map((tool) => [tool.name, { description: tool.description || "", inputSchema: tool.inputSchema }]));
155
+ const compressionEnabled = this.clientConfig.schemaCompression?.enabled ?? true;
156
+ const maxLen = this.clientConfig.schemaCompression?.maxDescriptionLength ?? 80;
119
157
  state.toolsCache = tools.map((tool) => ({
120
158
  name: tool.name,
121
- description: tool.description || "",
159
+ description: compressionEnabled
160
+ ? compressDescription(tool.description || "", maxLen)
161
+ : (tool.description || ""),
122
162
  requiredParams: this.extractRequiredParams(tool)
123
163
  }));
124
164
  this.markUsed(server);
125
165
  return state.toolsCache;
126
166
  }
167
+ async resolveIntent(intent) {
168
+ try {
169
+ // Lazily create the intent router
170
+ if (!this.intentRouter) {
171
+ const routingConfig = this.clientConfig.intentRouting;
172
+ const embeddingType = routingConfig?.embedding ?? "auto";
173
+ const provider = createEmbeddingProvider(embeddingType, { model: routingConfig?.model }, this.logger);
174
+ this.intentRouter = new IntentRouter(provider, this.logger, routingConfig?.minScore);
175
+ }
176
+ // Index tools if not already done
177
+ if (!this.intentRouter.isIndexed()) {
178
+ const allTools = {};
179
+ for (const serverName of Object.keys(this.servers)) {
180
+ try {
181
+ await this.getToolList(serverName);
182
+ const state = this.states.get(serverName);
183
+ if (state?.fullToolsMap) {
184
+ allTools[serverName] = [...state.fullToolsMap.entries()].map(([name, meta]) => ({
185
+ name,
186
+ description: meta.description,
187
+ inputSchema: meta.inputSchema
188
+ }));
189
+ }
190
+ }
191
+ catch (err) {
192
+ this.logger.warn(`[mcp-bridge] Intent routing: failed to index tools from ${serverName}:`, err);
193
+ }
194
+ }
195
+ await this.intentRouter.indexTools(allTools);
196
+ }
197
+ const match = await this.intentRouter.resolve(intent);
198
+ if (!match) {
199
+ return this.error("invalid_params", `No tool found matching intent: "${intent}"`);
200
+ }
201
+ return {
202
+ action: "intent",
203
+ intent,
204
+ match: { server: match.server, tool: match.tool, score: match.score },
205
+ alternatives: match.alternatives
206
+ };
207
+ }
208
+ catch (err) {
209
+ return this.error("mcp_error", `Intent resolution failed: ${err instanceof Error ? err.message : String(err)}`);
210
+ }
211
+ }
127
212
  getStatus() {
128
213
  const serverStatuses = Object.entries(this.servers).map(([name, config]) => {
129
214
  const state = this.states.get(name);
@@ -213,6 +298,7 @@ export class McpRouter {
213
298
  }
214
299
  state.initialized = false;
215
300
  state.toolsCache = undefined;
301
+ state.fullToolsMap = undefined;
216
302
  state.toolNames = [];
217
303
  }
218
304
  markUsed(server) {
@@ -240,6 +326,7 @@ export class McpRouter {
240
326
  return;
241
327
  state.initialized = false;
242
328
  state.toolsCache = undefined;
329
+ state.fullToolsMap = undefined;
243
330
  state.toolNames = [];
244
331
  };
245
332
  if (serverConfig.transport === "sse") {
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Compress tool descriptions to reduce token usage in router tool listings.
3
+ */
4
+ export declare function compressDescription(desc: string, maxLen?: number): string;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Compress tool descriptions to reduce token usage in router tool listings.
3
+ */
4
+ export function compressDescription(desc, maxLen = 80) {
5
+ if (desc.length <= maxLen) {
6
+ return desc;
7
+ }
8
+ // Try to cut at sentence boundary (". " before maxLen)
9
+ const searchArea = desc.slice(0, maxLen);
10
+ const sentenceEnd = searchArea.lastIndexOf(". ");
11
+ if (sentenceEnd > 0) {
12
+ return desc.slice(0, sentenceEnd + 1) + "\u2026";
13
+ }
14
+ // Fall back to word boundary
15
+ const lastSpace = searchArea.lastIndexOf(" ");
16
+ if (lastSpace > 0) {
17
+ return desc.slice(0, lastSpace) + "\u2026";
18
+ }
19
+ // No word boundary found, hard truncate
20
+ return desc.slice(0, maxLen) + "\u2026";
21
+ }
@@ -24,6 +24,15 @@ export interface McpClientConfig {
24
24
  requestTimeoutMs?: number;
25
25
  routerIdleTimeoutMs?: number;
26
26
  routerMaxConcurrent?: number;
27
+ schemaCompression?: {
28
+ enabled?: boolean;
29
+ maxDescriptionLength?: number;
30
+ };
31
+ intentRouting?: {
32
+ embedding?: "auto" | "gemini" | "openai" | "ollama" | "keyword";
33
+ model?: string;
34
+ minScore?: number;
35
+ };
27
36
  }
28
37
  export interface McpTool {
29
38
  name: string;
@@ -0,0 +1,17 @@
1
+ export interface VectorMetadata {
2
+ server: string;
3
+ tool: string;
4
+ description: string;
5
+ }
6
+ export declare class VectorStore {
7
+ private entries;
8
+ add(id: string, vector: number[], metadata: VectorMetadata): void;
9
+ search(queryVector: number[], topK: number): Array<{
10
+ id: string;
11
+ score: number;
12
+ metadata: VectorMetadata;
13
+ }>;
14
+ clear(): void;
15
+ size(): number;
16
+ }
17
+ export declare function cosineSimilarity(a: number[], b: number[]): number;
@@ -0,0 +1,43 @@
1
+ export class VectorStore {
2
+ entries = [];
3
+ add(id, vector, metadata) {
4
+ this.entries.push({ id, vector, metadata });
5
+ }
6
+ search(queryVector, topK) {
7
+ const scored = this.entries.map((entry) => ({
8
+ id: entry.id,
9
+ score: cosineSimilarity(queryVector, entry.vector),
10
+ metadata: entry.metadata
11
+ }));
12
+ scored.sort((a, b) => b.score - a.score);
13
+ return scored.slice(0, topK);
14
+ }
15
+ clear() {
16
+ this.entries = [];
17
+ }
18
+ size() {
19
+ return this.entries.length;
20
+ }
21
+ }
22
+ export function cosineSimilarity(a, b) {
23
+ const len = Math.min(a.length, b.length);
24
+ let dot = 0;
25
+ let normA = 0;
26
+ let normB = 0;
27
+ for (let i = 0; i < len; i++) {
28
+ dot += a[i] * b[i];
29
+ normA += a[i] * a[i];
30
+ normB += b[i] * b[i];
31
+ }
32
+ // Handle any extra dimensions (vectors may differ in length for keyword embeddings)
33
+ for (let i = len; i < a.length; i++) {
34
+ normA += a[i] * a[i];
35
+ }
36
+ for (let i = len; i < b.length; i++) {
37
+ normB += b[i] * b[i];
38
+ }
39
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
40
+ if (denom === 0)
41
+ return 0;
42
+ return dot / denom;
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwerk/mcp-bridge",
3
- "version": "1.2.3",
3
+ "version": "1.4.0",
4
4
  "description": "Standalone MCP server that multiplexes multiple MCP servers into one interface",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",