@aiwerk/mcp-bridge 1.3.0 → 1.5.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.
@@ -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
+ }
@@ -35,6 +35,19 @@ export type RouterDispatchResponse = {
35
35
  } | {
36
36
  action: "status";
37
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
+ }>;
38
51
  } | {
39
52
  error: RouterErrorCode;
40
53
  message: string;
@@ -54,10 +67,12 @@ export declare class McpRouter {
54
67
  private readonly idleTimeoutMs;
55
68
  private readonly maxConcurrent;
56
69
  private readonly states;
70
+ private intentRouter;
57
71
  constructor(servers: Record<string, McpServerConfig>, clientConfig: McpClientConfig, logger: Logger, transportRefs?: Partial<RouterTransportRefs>);
58
72
  static generateDescription(servers: Record<string, McpServerConfig>): string;
59
73
  dispatch(server?: string, action?: string, tool?: string, params?: any): Promise<RouterDispatchResponse>;
60
74
  getToolList(server: string): Promise<RouterToolHint[]>;
75
+ private resolveIntent;
61
76
  private getStatus;
62
77
  disconnectAll(): Promise<void>;
63
78
  private ensureConnected;
@@ -3,6 +3,9 @@ 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
5
  import { compressDescription } from "./schema-compression.js";
6
+ import { IntentRouter } from "./intent-router.js";
7
+ import { createEmbeddingProvider } from "./embeddings.js";
8
+ import { isToolAllowed, processResult } from "./security.js";
6
9
  const DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
7
10
  const DEFAULT_MAX_CONCURRENT = 5;
8
11
  export class McpRouter {
@@ -13,6 +16,7 @@ export class McpRouter {
13
16
  idleTimeoutMs;
14
17
  maxConcurrent;
15
18
  states = new Map();
19
+ intentRouter = null;
16
20
  constructor(servers, clientConfig, logger, transportRefs) {
17
21
  this.servers = servers;
18
22
  this.clientConfig = clientConfig;
@@ -45,6 +49,14 @@ export class McpRouter {
45
49
  if (normalizedAction === "status") {
46
50
  return this.getStatus();
47
51
  }
52
+ // Intent action: find server+tool from natural language
53
+ if (normalizedAction === "intent") {
54
+ const intent = params?.intent || tool;
55
+ if (!intent) {
56
+ return this.error("invalid_params", "intent string is required for action=intent");
57
+ }
58
+ return this.resolveIntent(intent);
59
+ }
48
60
  if (!server) {
49
61
  return this.error("invalid_params", "server is required");
50
62
  }
@@ -83,6 +95,10 @@ export class McpRouter {
83
95
  state.toolsCache = undefined;
84
96
  state.fullToolsMap = undefined;
85
97
  state.toolNames = [];
98
+ // Clear intent index so it re-indexes on next intent query
99
+ if (this.intentRouter) {
100
+ this.intentRouter.clearIndex();
101
+ }
86
102
  const tools = await this.getToolList(server);
87
103
  return { server, action: "refresh", refreshed: true, tools };
88
104
  }
@@ -91,7 +107,7 @@ export class McpRouter {
91
107
  }
92
108
  }
93
109
  if (normalizedAction !== "call") {
94
- return this.error("invalid_params", `action must be one of: list, call, refresh, schema`);
110
+ return this.error("invalid_params", `action must be one of: list, call, refresh, schema, intent`);
95
111
  }
96
112
  if (!tool) {
97
113
  return this.error("invalid_params", "tool is required for action=call");
@@ -106,6 +122,11 @@ export class McpRouter {
106
122
  if (!state.toolNames.includes(tool)) {
107
123
  return this.error("unknown_tool", `Tool '${tool}' not found on server '${server}'`, state.toolNames);
108
124
  }
125
+ // Defense in depth: double-check tool filter
126
+ const serverConfig = this.servers[server];
127
+ if (!isToolAllowed(tool, serverConfig)) {
128
+ return this.error("unknown_tool", `Tool '${tool}' is not allowed on server '${server}'`, state.toolNames);
129
+ }
109
130
  this.markUsed(server);
110
131
  const response = await state.transport.sendRequest({
111
132
  jsonrpc: "2.0",
@@ -118,7 +139,9 @@ export class McpRouter {
118
139
  if (response.error) {
119
140
  return this.error("mcp_error", response.error.message, undefined, response.error.code);
120
141
  }
121
- return { server, action: "call", tool, result: response.result };
142
+ // Security pipeline: truncate sanitize trust-tag
143
+ const result = processResult(response.result, server, serverConfig, this.clientConfig);
144
+ return { server, action: "call", tool, result };
122
145
  }
123
146
  catch (error) {
124
147
  return this.error("mcp_error", error instanceof Error ? error.message : String(error));
@@ -133,7 +156,9 @@ export class McpRouter {
133
156
  this.markUsed(server);
134
157
  return state.toolsCache;
135
158
  }
136
- const tools = await fetchToolsList(state.transport);
159
+ const allTools = await fetchToolsList(state.transport);
160
+ const serverConfig = this.servers[server];
161
+ const tools = allTools.filter((tool) => isToolAllowed(tool.name, serverConfig));
137
162
  state.toolNames = tools.map((tool) => tool.name);
138
163
  // Store full tool metadata for action=schema
139
164
  state.fullToolsMap = new Map(tools.map((tool) => [tool.name, { description: tool.description || "", inputSchema: tool.inputSchema }]));
@@ -149,6 +174,51 @@ export class McpRouter {
149
174
  this.markUsed(server);
150
175
  return state.toolsCache;
151
176
  }
177
+ async resolveIntent(intent) {
178
+ try {
179
+ // Lazily create the intent router
180
+ if (!this.intentRouter) {
181
+ const routingConfig = this.clientConfig.intentRouting;
182
+ const embeddingType = routingConfig?.embedding ?? "auto";
183
+ const provider = createEmbeddingProvider(embeddingType, { model: routingConfig?.model }, this.logger);
184
+ this.intentRouter = new IntentRouter(provider, this.logger, routingConfig?.minScore);
185
+ }
186
+ // Index tools if not already done
187
+ if (!this.intentRouter.isIndexed()) {
188
+ const allTools = {};
189
+ for (const serverName of Object.keys(this.servers)) {
190
+ try {
191
+ await this.getToolList(serverName);
192
+ const state = this.states.get(serverName);
193
+ if (state?.fullToolsMap) {
194
+ allTools[serverName] = [...state.fullToolsMap.entries()].map(([name, meta]) => ({
195
+ name,
196
+ description: meta.description,
197
+ inputSchema: meta.inputSchema
198
+ }));
199
+ }
200
+ }
201
+ catch (err) {
202
+ this.logger.warn(`[mcp-bridge] Intent routing: failed to index tools from ${serverName}:`, err);
203
+ }
204
+ }
205
+ await this.intentRouter.indexTools(allTools);
206
+ }
207
+ const match = await this.intentRouter.resolve(intent);
208
+ if (!match) {
209
+ return this.error("invalid_params", `No tool found matching intent: "${intent}"`);
210
+ }
211
+ return {
212
+ action: "intent",
213
+ intent,
214
+ match: { server: match.server, tool: match.tool, score: match.score },
215
+ alternatives: match.alternatives
216
+ };
217
+ }
218
+ catch (err) {
219
+ return this.error("mcp_error", `Intent resolution failed: ${err instanceof Error ? err.message : String(err)}`);
220
+ }
221
+ }
152
222
  getStatus() {
153
223
  const serverStatuses = Object.entries(this.servers).map(([name, config]) => {
154
224
  const state = this.states.get(name);
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Security processing for MCP tool results.
3
+ *
4
+ * Pipeline order: truncate → sanitize → trust-tag
5
+ */
6
+ import type { McpServerConfig, McpClientConfig } from "./types.js";
7
+ /**
8
+ * Sanitize a tool result by stripping HTML and prompt injection patterns.
9
+ */
10
+ export declare function sanitizeResult(result: any): any;
11
+ /**
12
+ * Check if a tool is allowed by the server's toolFilter config.
13
+ * Returns true if the tool should be visible/callable.
14
+ */
15
+ export declare function isToolAllowed(toolName: string, serverConfig: McpServerConfig): boolean;
16
+ /**
17
+ * Apply max result size truncation.
18
+ * Returns the result as-is or a truncation wrapper.
19
+ */
20
+ export declare function applyMaxResultSize(result: any, serverConfig: McpServerConfig, clientConfig: McpClientConfig): any;
21
+ /**
22
+ * Apply trust level wrapping/sanitization.
23
+ */
24
+ export declare function applyTrustLevel(result: any, serverName: string, serverConfig: McpServerConfig): any;
25
+ /**
26
+ * Full security pipeline: truncate → sanitize → trust-tag
27
+ */
28
+ export declare function processResult(result: any, serverName: string, serverConfig: McpServerConfig, clientConfig: McpClientConfig): any;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Security processing for MCP tool results.
3
+ *
4
+ * Pipeline order: truncate → sanitize → trust-tag
5
+ */
6
+ // Prompt injection patterns to strip (case-insensitive)
7
+ const INJECTION_PATTERNS = [
8
+ /ignore\s+(all\s+)?previous\s+instructions/gi,
9
+ /ignore\s+(all\s+)?prior\s+instructions/gi,
10
+ /disregard\s+(all\s+)?previous\s+instructions/gi,
11
+ /you\s+are\s+now\b/gi,
12
+ /^system\s*:/gim,
13
+ /\bact\s+as\s+(a|an)\s+/gi,
14
+ /pretend\s+you\s+are\b/gi,
15
+ /from\s+now\s+on\s+you\s+are\b/gi,
16
+ /new\s+instructions\s*:/gi,
17
+ /override\s+(all\s+)?instructions/gi,
18
+ ];
19
+ function stripHtmlTags(text) {
20
+ return text.replace(/<[^>]*>/g, "");
21
+ }
22
+ function stripInjectionPatterns(text) {
23
+ let result = text;
24
+ for (const pattern of INJECTION_PATTERNS) {
25
+ // Reset lastIndex for global regexes
26
+ pattern.lastIndex = 0;
27
+ result = result.replace(pattern, "");
28
+ }
29
+ return result;
30
+ }
31
+ function sanitizeString(text) {
32
+ return stripInjectionPatterns(stripHtmlTags(text));
33
+ }
34
+ /**
35
+ * Sanitize a tool result by stripping HTML and prompt injection patterns.
36
+ */
37
+ export function sanitizeResult(result) {
38
+ if (typeof result === "string") {
39
+ return sanitizeString(result);
40
+ }
41
+ if (Array.isArray(result)) {
42
+ return result.map((item) => sanitizeResult(item));
43
+ }
44
+ if (result !== null && typeof result === "object") {
45
+ // MCP standard content array
46
+ if (Array.isArray(result.content)) {
47
+ return {
48
+ ...result,
49
+ content: result.content.map((item) => {
50
+ if (item.type === "text" && typeof item.text === "string") {
51
+ return { ...item, text: sanitizeString(item.text) };
52
+ }
53
+ return item;
54
+ }),
55
+ };
56
+ }
57
+ // Recursively sanitize object values
58
+ const sanitized = {};
59
+ for (const [key, value] of Object.entries(result)) {
60
+ sanitized[key] = sanitizeResult(value);
61
+ }
62
+ return sanitized;
63
+ }
64
+ return result;
65
+ }
66
+ /**
67
+ * Check if a tool is allowed by the server's toolFilter config.
68
+ * Returns true if the tool should be visible/callable.
69
+ */
70
+ export function isToolAllowed(toolName, serverConfig) {
71
+ const filter = serverConfig.toolFilter;
72
+ if (!filter)
73
+ return true;
74
+ const { allow, deny } = filter;
75
+ if (allow && allow.length > 0) {
76
+ // Whitelist mode: only allowed tools, minus denied
77
+ if (!allow.includes(toolName))
78
+ return false;
79
+ }
80
+ if (deny && deny.length > 0) {
81
+ if (deny.includes(toolName))
82
+ return false;
83
+ }
84
+ return true;
85
+ }
86
+ /**
87
+ * Apply max result size truncation.
88
+ * Returns the result as-is or a truncation wrapper.
89
+ */
90
+ export function applyMaxResultSize(result, serverConfig, clientConfig) {
91
+ const limit = serverConfig.maxResultChars ?? clientConfig.maxResultChars;
92
+ if (limit === undefined)
93
+ return result;
94
+ const serialized = JSON.stringify(result);
95
+ if (serialized.length <= limit)
96
+ return result;
97
+ return {
98
+ _truncated: true,
99
+ _originalLength: serialized.length,
100
+ result: serialized.slice(0, limit),
101
+ };
102
+ }
103
+ /**
104
+ * Apply trust level wrapping/sanitization.
105
+ */
106
+ export function applyTrustLevel(result, serverName, serverConfig) {
107
+ const trust = serverConfig.trust ?? "trusted";
108
+ switch (trust) {
109
+ case "trusted":
110
+ return result;
111
+ case "untrusted":
112
+ return { _trust: "untrusted", _server: serverName, result };
113
+ case "sanitize":
114
+ return sanitizeResult(result);
115
+ default:
116
+ return result;
117
+ }
118
+ }
119
+ /**
120
+ * Full security pipeline: truncate → sanitize → trust-tag
121
+ */
122
+ export function processResult(result, serverName, serverConfig, clientConfig) {
123
+ let processed = applyMaxResultSize(result, serverConfig, clientConfig);
124
+ // Sanitize step (only for trust=sanitize, handled inside applyTrustLevel)
125
+ processed = applyTrustLevel(processed, serverName, serverConfig);
126
+ return processed;
127
+ }
@@ -14,6 +14,12 @@ export interface McpServerConfig {
14
14
  args?: string[];
15
15
  env?: Record<string, string>;
16
16
  framing?: "auto" | "lsp" | "newline";
17
+ trust?: "trusted" | "untrusted" | "sanitize";
18
+ toolFilter?: {
19
+ deny?: string[];
20
+ allow?: string[];
21
+ };
22
+ maxResultChars?: number;
17
23
  }
18
24
  export interface McpClientConfig {
19
25
  servers: Record<string, McpServerConfig>;
@@ -28,6 +34,12 @@ export interface McpClientConfig {
28
34
  enabled?: boolean;
29
35
  maxDescriptionLength?: number;
30
36
  };
37
+ intentRouting?: {
38
+ embedding?: "auto" | "gemini" | "openai" | "ollama" | "keyword";
39
+ model?: string;
40
+ minScore?: number;
41
+ };
42
+ maxResultChars?: number;
31
43
  }
32
44
  export interface McpTool {
33
45
  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.3.0",
3
+ "version": "1.5.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",