@aiwerk/mcp-bridge 1.4.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.
- package/dist/src/mcp-router.js +12 -2
- package/dist/src/security.d.ts +28 -0
- package/dist/src/security.js +127 -0
- package/dist/src/types.d.ts +7 -0
- package/package.json +1 -1
package/dist/src/mcp-router.js
CHANGED
|
@@ -5,6 +5,7 @@ import { fetchToolsList, initializeProtocol, PACKAGE_VERSION } from "./protocol.
|
|
|
5
5
|
import { compressDescription } from "./schema-compression.js";
|
|
6
6
|
import { IntentRouter } from "./intent-router.js";
|
|
7
7
|
import { createEmbeddingProvider } from "./embeddings.js";
|
|
8
|
+
import { isToolAllowed, processResult } from "./security.js";
|
|
8
9
|
const DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
9
10
|
const DEFAULT_MAX_CONCURRENT = 5;
|
|
10
11
|
export class McpRouter {
|
|
@@ -121,6 +122,11 @@ export class McpRouter {
|
|
|
121
122
|
if (!state.toolNames.includes(tool)) {
|
|
122
123
|
return this.error("unknown_tool", `Tool '${tool}' not found on server '${server}'`, state.toolNames);
|
|
123
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
|
+
}
|
|
124
130
|
this.markUsed(server);
|
|
125
131
|
const response = await state.transport.sendRequest({
|
|
126
132
|
jsonrpc: "2.0",
|
|
@@ -133,7 +139,9 @@ export class McpRouter {
|
|
|
133
139
|
if (response.error) {
|
|
134
140
|
return this.error("mcp_error", response.error.message, undefined, response.error.code);
|
|
135
141
|
}
|
|
136
|
-
|
|
142
|
+
// Security pipeline: truncate → sanitize → trust-tag
|
|
143
|
+
const result = processResult(response.result, server, serverConfig, this.clientConfig);
|
|
144
|
+
return { server, action: "call", tool, result };
|
|
137
145
|
}
|
|
138
146
|
catch (error) {
|
|
139
147
|
return this.error("mcp_error", error instanceof Error ? error.message : String(error));
|
|
@@ -148,7 +156,9 @@ export class McpRouter {
|
|
|
148
156
|
this.markUsed(server);
|
|
149
157
|
return state.toolsCache;
|
|
150
158
|
}
|
|
151
|
-
const
|
|
159
|
+
const allTools = await fetchToolsList(state.transport);
|
|
160
|
+
const serverConfig = this.servers[server];
|
|
161
|
+
const tools = allTools.filter((tool) => isToolAllowed(tool.name, serverConfig));
|
|
152
162
|
state.toolNames = tools.map((tool) => tool.name);
|
|
153
163
|
// Store full tool metadata for action=schema
|
|
154
164
|
state.fullToolsMap = new Map(tools.map((tool) => [tool.name, { description: tool.description || "", inputSchema: tool.inputSchema }]));
|
|
@@ -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
|
+
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -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>;
|
|
@@ -33,6 +39,7 @@ export interface McpClientConfig {
|
|
|
33
39
|
model?: string;
|
|
34
40
|
minScore?: number;
|
|
35
41
|
};
|
|
42
|
+
maxResultChars?: number;
|
|
36
43
|
}
|
|
37
44
|
export interface McpTool {
|
|
38
45
|
name: string;
|