@mcp-bastion/core 1.0.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/guard.d.ts +25 -0
- package/dist/guard.d.ts.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +218 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/rate-limit.d.ts +19 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.test.d.ts +2 -0
- package/dist/rate-limit.test.d.ts.map +1 -0
- package/package.json +51 -0
package/dist/guard.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP-Bastion proxy wrapper for MCP server handlers.
|
|
3
|
+
* Wraps CallTool and ReadResource; rate limit in-process, ML via sidecar.
|
|
4
|
+
*/
|
|
5
|
+
import type { CallToolRequest, CallToolResult, ReadResourceRequest, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
export interface McpBastionOptions {
|
|
7
|
+
maxIterations?: number;
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
enableRateLimit?: boolean;
|
|
10
|
+
sidecarUrl?: string;
|
|
11
|
+
enablePromptGuard?: boolean;
|
|
12
|
+
enablePiiRedaction?: boolean;
|
|
13
|
+
}
|
|
14
|
+
type CallToolHandler = (request: CallToolRequest) => Promise<CallToolResult>;
|
|
15
|
+
type ReadResourceHandler = (request: ReadResourceRequest) => Promise<ReadResourceResult>;
|
|
16
|
+
/** Wraps CallTool handler. Rate limit in-process; prompt guard via sidecar. */
|
|
17
|
+
export declare function wrapCallToolHandler(handler: CallToolHandler, options?: McpBastionOptions): CallToolHandler;
|
|
18
|
+
/** Wraps ReadResource handler. PII redaction via sidecar when enabled. */
|
|
19
|
+
export declare function wrapReadResourceHandler(handler: ReadResourceHandler, options?: McpBastionOptions): ReadResourceHandler;
|
|
20
|
+
/** Patches setRequestHandler to wrap CallTool and ReadResource handlers. */
|
|
21
|
+
export declare function wrapWithMcpBastion<T extends {
|
|
22
|
+
setRequestHandler: (schema: unknown, handler: unknown) => void;
|
|
23
|
+
}>(server: T, options?: McpBastionOptions): T;
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,eAAe,EACf,cAAc,EACd,mBAAmB,EACnB,kBAAkB,EACnB,MAAM,oCAAoC,CAAC;AAI5C,MAAM,WAAW,iBAAiB;IAChC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAkCD,KAAK,eAAe,GAAG,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;AAC7E,KAAK,mBAAmB,GAAG,CACzB,OAAO,EAAE,mBAAmB,KACzB,OAAO,CAAC,kBAAkB,CAAC,CAAC;AAEjC,+EAA+E;AAC/E,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,eAAe,EACxB,OAAO,GAAE,iBAAsB,GAC9B,eAAe,CAiEjB;AAED,0EAA0E;AAC1E,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,mBAAmB,EAC5B,OAAO,GAAE,iBAAsB,GAC9B,mBAAmB,CAuBrB;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,CAAC,SAAS;IAAE,iBAAiB,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;CAAE,EAC7G,MAAM,EAAE,CAAC,EACT,OAAO,GAAE,iBAAsB,GAC9B,CAAC,CAyBH"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP-Bastion Core: Security middleware for MCP servers.
|
|
3
|
+
* Rate limiting in-process; prompt injection and PII via Python sidecar.
|
|
4
|
+
*/
|
|
5
|
+
export { wrapWithMcpBastion, wrapCallToolHandler, wrapReadResourceHandler, } from "./guard.js";
|
|
6
|
+
export type { McpBastionOptions } from "./guard.js";
|
|
7
|
+
export { TokenBucketRateLimiter } from "./rate-limit.js";
|
|
8
|
+
export { logger, setLogLevel } from "./logger.js";
|
|
9
|
+
export type { LogLevel } from "./logger.js";
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,uBAAuB,GACxB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAClD,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
const DEFAULT_MAX_ITERATIONS = 15;
|
|
2
|
+
const DEFAULT_TIMEOUT_MS = 6e4;
|
|
3
|
+
class TokenBucketRateLimiter {
|
|
4
|
+
maxIterations;
|
|
5
|
+
timeoutMs;
|
|
6
|
+
sessions = /* @__PURE__ */ new Map();
|
|
7
|
+
constructor(maxIterations = DEFAULT_MAX_ITERATIONS, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
8
|
+
this.maxIterations = maxIterations;
|
|
9
|
+
this.timeoutMs = timeoutMs;
|
|
10
|
+
}
|
|
11
|
+
getSessionKey(requestId, sessionId) {
|
|
12
|
+
return sessionId ?? requestId ?? "default";
|
|
13
|
+
}
|
|
14
|
+
cleanupExpired(key) {
|
|
15
|
+
const state = this.sessions.get(key);
|
|
16
|
+
if (!state) return;
|
|
17
|
+
const elapsed = Date.now() - state.startedAt;
|
|
18
|
+
if (elapsed > this.timeoutMs) {
|
|
19
|
+
this.sessions.delete(key);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
checkIteration(requestId, sessionId) {
|
|
23
|
+
const key = this.getSessionKey(requestId, sessionId);
|
|
24
|
+
this.cleanupExpired(key);
|
|
25
|
+
const state = this.sessions.get(key);
|
|
26
|
+
if (!state) {
|
|
27
|
+
return { allowed: true };
|
|
28
|
+
}
|
|
29
|
+
const elapsed = Date.now() - state.startedAt;
|
|
30
|
+
if (elapsed > this.timeoutMs) {
|
|
31
|
+
this.sessions.delete(key);
|
|
32
|
+
return { allowed: false, error: "Session timeout exceeded (60s limit)" };
|
|
33
|
+
}
|
|
34
|
+
if (state.iterations >= this.maxIterations) {
|
|
35
|
+
return {
|
|
36
|
+
allowed: false,
|
|
37
|
+
error: `Maximum iterations exceeded (${this.maxIterations} limit)`
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return { allowed: true };
|
|
41
|
+
}
|
|
42
|
+
consumeIteration(requestId, sessionId) {
|
|
43
|
+
const key = this.getSessionKey(requestId, sessionId);
|
|
44
|
+
let state = this.sessions.get(key);
|
|
45
|
+
if (!state) {
|
|
46
|
+
state = { iterations: 0, startedAt: Date.now() };
|
|
47
|
+
this.sessions.set(key, state);
|
|
48
|
+
}
|
|
49
|
+
state.iterations += 1;
|
|
50
|
+
}
|
|
51
|
+
resetSession(requestId, sessionId) {
|
|
52
|
+
const key = this.getSessionKey(requestId, sessionId);
|
|
53
|
+
this.sessions.delete(key);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const PREFIX = "[mcp-bastion]";
|
|
57
|
+
const LEVEL_ORDER = {
|
|
58
|
+
debug: 0,
|
|
59
|
+
info: 1,
|
|
60
|
+
warn: 2,
|
|
61
|
+
error: 3
|
|
62
|
+
};
|
|
63
|
+
let currentLevel = "info";
|
|
64
|
+
function setLogLevel(level) {
|
|
65
|
+
currentLevel = level;
|
|
66
|
+
}
|
|
67
|
+
function shouldLog(level) {
|
|
68
|
+
return LEVEL_ORDER[level] >= LEVEL_ORDER[currentLevel];
|
|
69
|
+
}
|
|
70
|
+
const logger = {
|
|
71
|
+
debug(msg, ...args) {
|
|
72
|
+
if (shouldLog("debug")) console.debug(PREFIX, msg, ...args);
|
|
73
|
+
},
|
|
74
|
+
info(msg, ...args) {
|
|
75
|
+
if (shouldLog("info")) console.info(PREFIX, msg, ...args);
|
|
76
|
+
},
|
|
77
|
+
warn(msg, ...args) {
|
|
78
|
+
if (shouldLog("warn")) console.warn(PREFIX, msg, ...args);
|
|
79
|
+
},
|
|
80
|
+
error(msg, ...args) {
|
|
81
|
+
if (shouldLog("error")) console.error(PREFIX, msg, ...args);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const DEFAULT_OPTIONS = {
|
|
85
|
+
maxIterations: 15,
|
|
86
|
+
timeoutMs: 6e4,
|
|
87
|
+
enableRateLimit: true,
|
|
88
|
+
sidecarUrl: "",
|
|
89
|
+
enablePromptGuard: false,
|
|
90
|
+
enablePiiRedaction: false
|
|
91
|
+
};
|
|
92
|
+
function createMcpError(code, message) {
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: "text", text: `[MCP-Bastion] ${message}` }],
|
|
95
|
+
isError: true
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
async function callSidecar(url, endpoint, payload) {
|
|
99
|
+
const res = await fetch(`${url}/${endpoint}`, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: { "Content-Type": "application/json" },
|
|
102
|
+
body: JSON.stringify(payload)
|
|
103
|
+
});
|
|
104
|
+
if (!res.ok) {
|
|
105
|
+
throw new Error(`Sidecar ${endpoint} failed: ${res.status}`);
|
|
106
|
+
}
|
|
107
|
+
return res.json();
|
|
108
|
+
}
|
|
109
|
+
function wrapCallToolHandler(handler, options = {}) {
|
|
110
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
111
|
+
const rateLimiter = new TokenBucketRateLimiter(
|
|
112
|
+
opts.maxIterations,
|
|
113
|
+
opts.timeoutMs
|
|
114
|
+
);
|
|
115
|
+
return async (request) => {
|
|
116
|
+
const requestId = String(request.id ?? "");
|
|
117
|
+
const sessionId = request.params?._meta?.["session_id"];
|
|
118
|
+
if (opts.enableRateLimit) {
|
|
119
|
+
const { allowed, error } = rateLimiter.checkIteration(
|
|
120
|
+
requestId,
|
|
121
|
+
sessionId
|
|
122
|
+
);
|
|
123
|
+
if (!allowed) {
|
|
124
|
+
logger.warn("rate_limit_blocked", requestId, sessionId, error);
|
|
125
|
+
return createMcpError(-32002, error ?? "Rate limit exceeded");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (opts.enablePromptGuard && opts.sidecarUrl) {
|
|
129
|
+
try {
|
|
130
|
+
const args = request.params?.arguments ?? {};
|
|
131
|
+
const text = JSON.stringify(args);
|
|
132
|
+
const result2 = await callSidecar(
|
|
133
|
+
opts.sidecarUrl,
|
|
134
|
+
"prompt-guard",
|
|
135
|
+
{ text }
|
|
136
|
+
);
|
|
137
|
+
if (result2?.malicious) {
|
|
138
|
+
logger.warn("prompt_injection_blocked", requestId);
|
|
139
|
+
return createMcpError(
|
|
140
|
+
-32001,
|
|
141
|
+
"Request blocked: potential prompt injection detected"
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
} catch (err) {
|
|
145
|
+
logger.warn("prompt_guard_sidecar_unavailable", err);
|
|
146
|
+
return createMcpError(-32001, "Prompt guard sidecar unavailable");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
rateLimiter.consumeIteration(requestId, sessionId);
|
|
150
|
+
let result = await handler(request);
|
|
151
|
+
if (opts.enablePiiRedaction && opts.sidecarUrl && result?.content) {
|
|
152
|
+
try {
|
|
153
|
+
const redacted = await callSidecar(
|
|
154
|
+
opts.sidecarUrl,
|
|
155
|
+
"pii-redact",
|
|
156
|
+
{ content: result.content }
|
|
157
|
+
);
|
|
158
|
+
if (redacted?.content) {
|
|
159
|
+
result = { ...result, content: redacted.content };
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function wrapReadResourceHandler(handler, options = {}) {
|
|
169
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
170
|
+
return async (request) => {
|
|
171
|
+
const result = await handler(request);
|
|
172
|
+
if (opts.enablePiiRedaction && opts.sidecarUrl && result?.contents) {
|
|
173
|
+
try {
|
|
174
|
+
const redacted = await callSidecar(
|
|
175
|
+
opts.sidecarUrl,
|
|
176
|
+
"pii-redact",
|
|
177
|
+
{ content: result.contents }
|
|
178
|
+
);
|
|
179
|
+
if (redacted?.content) {
|
|
180
|
+
return { ...result, contents: redacted.content };
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return result;
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function wrapWithMcpBastion(server, options = {}) {
|
|
190
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
191
|
+
const original = server.setRequestHandler.bind(server);
|
|
192
|
+
server.setRequestHandler = function(schema, handler) {
|
|
193
|
+
const schemaStr = String(schema?.name ?? schema);
|
|
194
|
+
if (schemaStr.includes("CallTool") || schemaStr.includes("tools/call")) {
|
|
195
|
+
return original(
|
|
196
|
+
schema,
|
|
197
|
+
wrapCallToolHandler(handler, opts)
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
if (schemaStr.includes("ReadResource") || schemaStr.includes("resources/read")) {
|
|
201
|
+
return original(
|
|
202
|
+
schema,
|
|
203
|
+
wrapReadResourceHandler(handler, opts)
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return original(schema, handler);
|
|
207
|
+
};
|
|
208
|
+
return server;
|
|
209
|
+
}
|
|
210
|
+
export {
|
|
211
|
+
TokenBucketRateLimiter,
|
|
212
|
+
logger,
|
|
213
|
+
setLogLevel,
|
|
214
|
+
wrapCallToolHandler,
|
|
215
|
+
wrapReadResourceHandler,
|
|
216
|
+
wrapWithMcpBastion
|
|
217
|
+
};
|
|
218
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/rate-limit.ts","../src/logger.ts","../src/guard.ts"],"sourcesContent":["/**\r\n * Token bucket rate limiter for MCP tool calls.\r\n * Iteration cap (15), timeout (60s).\r\n */\r\n\r\nconst DEFAULT_MAX_ITERATIONS = 15;\r\nconst DEFAULT_TIMEOUT_MS = 60_000;\r\n\r\ninterface SessionState {\r\n iterations: number;\r\n startedAt: number;\r\n}\r\n\r\nexport class TokenBucketRateLimiter {\r\n private readonly maxIterations: number;\r\n private readonly timeoutMs: number;\r\n private readonly sessions = new Map<string, SessionState>();\r\n\r\n constructor(\r\n maxIterations = DEFAULT_MAX_ITERATIONS,\r\n timeoutMs = DEFAULT_TIMEOUT_MS\r\n ) {\r\n this.maxIterations = maxIterations;\r\n this.timeoutMs = timeoutMs;\r\n }\r\n\r\n private getSessionKey(requestId?: string | null, sessionId?: string | null): string {\r\n return sessionId ?? requestId ?? \"default\";\r\n }\r\n\r\n private cleanupExpired(key: string): void {\r\n const state = this.sessions.get(key);\r\n if (!state) return;\r\n const elapsed = Date.now() - state.startedAt;\r\n if (elapsed > this.timeoutMs) {\r\n this.sessions.delete(key);\r\n }\r\n }\r\n\r\n checkIteration(\r\n requestId?: string | null,\r\n sessionId?: string | null\r\n ): { allowed: boolean; error?: string } {\r\n const key = this.getSessionKey(requestId, sessionId);\r\n this.cleanupExpired(key);\r\n\r\n const state = this.sessions.get(key);\r\n if (!state) {\r\n return { allowed: true };\r\n }\r\n\r\n const elapsed = Date.now() - state.startedAt;\r\n if (elapsed > this.timeoutMs) {\r\n this.sessions.delete(key);\r\n return { allowed: false, error: \"Session timeout exceeded (60s limit)\" };\r\n }\r\n\r\n if (state.iterations >= this.maxIterations) {\r\n return {\r\n allowed: false,\r\n error: `Maximum iterations exceeded (${this.maxIterations} limit)`,\r\n };\r\n }\r\n\r\n return { allowed: true };\r\n }\r\n\r\n consumeIteration(\r\n requestId?: string | null,\r\n sessionId?: string | null\r\n ): void {\r\n const key = this.getSessionKey(requestId, sessionId);\r\n let state = this.sessions.get(key);\r\n if (!state) {\r\n state = { iterations: 0, startedAt: Date.now() };\r\n this.sessions.set(key, state);\r\n }\r\n state.iterations += 1;\r\n }\r\n\r\n resetSession(requestId?: string | null, sessionId?: string | null): void {\r\n const key = this.getSessionKey(requestId, sessionId);\r\n this.sessions.delete(key);\r\n }\r\n}\r\n","/**\r\n * Simple logger for MCP-Bastion. Uses console with level filtering.\r\n */\r\n\r\nconst PREFIX = \"[mcp-bastion]\";\r\n\r\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\r\n\r\nconst LEVEL_ORDER: Record<LogLevel, number> = {\r\n debug: 0,\r\n info: 1,\r\n warn: 2,\r\n error: 3,\r\n};\r\n\r\nlet currentLevel: LogLevel = \"info\";\r\n\r\nexport function setLogLevel(level: LogLevel): void {\r\n currentLevel = level;\r\n}\r\n\r\nfunction shouldLog(level: LogLevel): boolean {\r\n return LEVEL_ORDER[level] >= LEVEL_ORDER[currentLevel];\r\n}\r\n\r\nexport const logger = {\r\n debug(msg: string, ...args: unknown[]): void {\r\n if (shouldLog(\"debug\")) console.debug(PREFIX, msg, ...args);\r\n },\r\n info(msg: string, ...args: unknown[]): void {\r\n if (shouldLog(\"info\")) console.info(PREFIX, msg, ...args);\r\n },\r\n warn(msg: string, ...args: unknown[]): void {\r\n if (shouldLog(\"warn\")) console.warn(PREFIX, msg, ...args);\r\n },\r\n error(msg: string, ...args: unknown[]): void {\r\n if (shouldLog(\"error\")) console.error(PREFIX, msg, ...args);\r\n },\r\n};\r\n","/**\r\n * MCP-Bastion proxy wrapper for MCP server handlers.\r\n * Wraps CallTool and ReadResource; rate limit in-process, ML via sidecar.\r\n */\r\n\r\nimport type {\r\n CallToolRequest,\r\n CallToolResult,\r\n ReadResourceRequest,\r\n ReadResourceResult,\r\n} from \"@modelcontextprotocol/sdk/types.js\";\r\nimport { TokenBucketRateLimiter } from \"./rate-limit.js\";\r\nimport { logger } from \"./logger.js\";\r\n\r\nexport interface McpBastionOptions {\r\n maxIterations?: number;\r\n timeoutMs?: number;\r\n enableRateLimit?: boolean;\r\n sidecarUrl?: string;\r\n enablePromptGuard?: boolean;\r\n enablePiiRedaction?: boolean;\r\n}\r\n\r\nconst DEFAULT_OPTIONS: Required<McpBastionOptions> = {\r\n maxIterations: 15,\r\n timeoutMs: 60_000,\r\n enableRateLimit: true,\r\n sidecarUrl: \"\",\r\n enablePromptGuard: false,\r\n enablePiiRedaction: false,\r\n};\r\n\r\nfunction createMcpError(code: number, message: string): CallToolResult {\r\n return {\r\n content: [{ type: \"text\", text: `[MCP-Bastion] ${message}` }],\r\n isError: true,\r\n };\r\n}\r\n\r\nasync function callSidecar(\r\n url: string,\r\n endpoint: \"prompt-guard\" | \"pii-redact\",\r\n payload: unknown\r\n): Promise<unknown> {\r\n const res = await fetch(`${url}/${endpoint}`, {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/json\" },\r\n body: JSON.stringify(payload),\r\n });\r\n if (!res.ok) {\r\n throw new Error(`Sidecar ${endpoint} failed: ${res.status}`);\r\n }\r\n return res.json();\r\n}\r\n\r\ntype CallToolHandler = (request: CallToolRequest) => Promise<CallToolResult>;\r\ntype ReadResourceHandler = (\r\n request: ReadResourceRequest\r\n) => Promise<ReadResourceResult>;\r\n\r\n/** Wraps CallTool handler. Rate limit in-process; prompt guard via sidecar. */\r\nexport function wrapCallToolHandler(\r\n handler: CallToolHandler,\r\n options: McpBastionOptions = {}\r\n): CallToolHandler {\r\n const opts = { ...DEFAULT_OPTIONS, ...options };\r\n const rateLimiter = new TokenBucketRateLimiter(\r\n opts.maxIterations,\r\n opts.timeoutMs\r\n );\r\n\r\n return async (request: CallToolRequest): Promise<CallToolResult> => {\r\n const requestId = String((request as { id?: string | number }).id ?? \"\");\r\n const sessionId = (request.params?._meta as Record<string, string>)?.[\"session_id\"];\r\n\r\n if (opts.enableRateLimit) {\r\n const { allowed, error } = rateLimiter.checkIteration(\r\n requestId,\r\n sessionId\r\n );\r\n if (!allowed) {\r\n logger.warn(\"rate_limit_blocked\", requestId, sessionId, error);\r\n return createMcpError(-32002, error ?? \"Rate limit exceeded\");\r\n }\r\n }\r\n\r\n if (opts.enablePromptGuard && opts.sidecarUrl) {\r\n try {\r\n const args = request.params?.arguments ?? {};\r\n const text = JSON.stringify(args);\r\n const result = (await callSidecar(\r\n opts.sidecarUrl,\r\n \"prompt-guard\",\r\n { text }\r\n )) as { malicious?: boolean };\r\n if (result?.malicious) {\r\n logger.warn(\"prompt_injection_blocked\", requestId);\r\n return createMcpError(\r\n -32001,\r\n \"Request blocked: potential prompt injection detected\"\r\n );\r\n }\r\n } catch (err) {\r\n logger.warn(\"prompt_guard_sidecar_unavailable\", err);\r\n return createMcpError(-32001, \"Prompt guard sidecar unavailable\");\r\n }\r\n }\r\n\r\n rateLimiter.consumeIteration(requestId, sessionId);\r\n\r\n let result = await handler(request);\r\n\r\n if (opts.enablePiiRedaction && opts.sidecarUrl && result?.content) {\r\n try {\r\n const redacted = (await callSidecar(\r\n opts.sidecarUrl,\r\n \"pii-redact\",\r\n { content: result.content }\r\n )) as { content?: CallToolResult[\"content\"] };\r\n if (redacted?.content) {\r\n result = { ...result, content: redacted.content };\r\n }\r\n } catch {\r\n return result;\r\n }\r\n }\r\n\r\n return result;\r\n };\r\n}\r\n\r\n/** Wraps ReadResource handler. PII redaction via sidecar when enabled. */\r\nexport function wrapReadResourceHandler(\r\n handler: ReadResourceHandler,\r\n options: McpBastionOptions = {}\r\n): ReadResourceHandler {\r\n const opts = { ...DEFAULT_OPTIONS, ...options };\r\n\r\n return async (request: ReadResourceRequest): Promise<ReadResourceResult> => {\r\n const result = await handler(request);\r\n\r\n if (opts.enablePiiRedaction && opts.sidecarUrl && result?.contents) {\r\n try {\r\n const redacted = (await callSidecar(\r\n opts.sidecarUrl,\r\n \"pii-redact\",\r\n { content: result.contents }\r\n )) as { content?: ReadResourceResult[\"contents\"] };\r\n if (redacted?.content) {\r\n return { ...result, contents: redacted.content };\r\n }\r\n } catch {\r\n return result;\r\n }\r\n }\r\n\r\n return result;\r\n };\r\n}\r\n\r\n/** Patches setRequestHandler to wrap CallTool and ReadResource handlers. */\r\nexport function wrapWithMcpBastion<T extends { setRequestHandler: (schema: unknown, handler: unknown) => void }>(\r\n server: T,\r\n options: McpBastionOptions = {}\r\n): T {\r\n const opts = { ...DEFAULT_OPTIONS, ...options };\r\n const original = server.setRequestHandler.bind(server);\r\n\r\n server.setRequestHandler = function (\r\n schema: unknown,\r\n handler: unknown\r\n ): void {\r\n const schemaStr = String((schema as { name?: string })?.name ?? schema);\r\n if (schemaStr.includes(\"CallTool\") || schemaStr.includes(\"tools/call\")) {\r\n return original(\r\n schema,\r\n wrapCallToolHandler(handler as CallToolHandler, opts)\r\n );\r\n }\r\n if (schemaStr.includes(\"ReadResource\") || schemaStr.includes(\"resources/read\")) {\r\n return original(\r\n schema,\r\n wrapReadResourceHandler(handler as ReadResourceHandler, opts)\r\n );\r\n }\r\n return original(schema, handler);\r\n } as T[\"setRequestHandler\"];\r\n\r\n return server;\r\n}\r\n"],"names":["result"],"mappings":"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"}
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple logger for MCP-Bastion. Uses console with level filtering.
|
|
3
|
+
*/
|
|
4
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
5
|
+
export declare function setLogLevel(level: LogLevel): void;
|
|
6
|
+
export declare const logger: {
|
|
7
|
+
debug(msg: string, ...args: unknown[]): void;
|
|
8
|
+
info(msg: string, ...args: unknown[]): void;
|
|
9
|
+
warn(msg: string, ...args: unknown[]): void;
|
|
10
|
+
error(msg: string, ...args: unknown[]): void;
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAW3D,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAEjD;AAMD,eAAO,MAAM,MAAM;eACN,MAAM,WAAW,OAAO,EAAE,GAAG,IAAI;cAGlC,MAAM,WAAW,OAAO,EAAE,GAAG,IAAI;cAGjC,MAAM,WAAW,OAAO,EAAE,GAAG,IAAI;eAGhC,MAAM,WAAW,OAAO,EAAE,GAAG,IAAI;CAG7C,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token bucket rate limiter for MCP tool calls.
|
|
3
|
+
* Iteration cap (15), timeout (60s).
|
|
4
|
+
*/
|
|
5
|
+
export declare class TokenBucketRateLimiter {
|
|
6
|
+
private readonly maxIterations;
|
|
7
|
+
private readonly timeoutMs;
|
|
8
|
+
private readonly sessions;
|
|
9
|
+
constructor(maxIterations?: number, timeoutMs?: number);
|
|
10
|
+
private getSessionKey;
|
|
11
|
+
private cleanupExpired;
|
|
12
|
+
checkIteration(requestId?: string | null, sessionId?: string | null): {
|
|
13
|
+
allowed: boolean;
|
|
14
|
+
error?: string;
|
|
15
|
+
};
|
|
16
|
+
consumeIteration(requestId?: string | null, sessionId?: string | null): void;
|
|
17
|
+
resetSession(requestId?: string | null, sessionId?: string | null): void;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=rate-limit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAUH,qBAAa,sBAAsB;IACjC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmC;gBAG1D,aAAa,SAAyB,EACtC,SAAS,SAAqB;IAMhC,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,cAAc;IAStB,cAAc,CACZ,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,EACzB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GACxB;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE;IAyBvC,gBAAgB,CACd,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,EACzB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GACxB,IAAI;IAUP,YAAY,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;CAIzE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.test.d.ts","sourceRoot":"","sources":["../src/rate-limit.test.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mcp-bastion/core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Security middleware for MCP servers protecting LLM agents from prompt injection, resource exhaustion, and PII leakage",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "vite build && tsc --emitDeclarationOnly --outDir dist",
|
|
21
|
+
"dev": "vite build --watch",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:watch": "vitest",
|
|
24
|
+
"prepublishOnly": "npm run build"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"mcp",
|
|
28
|
+
"security",
|
|
29
|
+
"middleware",
|
|
30
|
+
"llm",
|
|
31
|
+
"prompt-injection",
|
|
32
|
+
"pii"
|
|
33
|
+
],
|
|
34
|
+
"author": "Viquar Khan",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/vaquarkhan/MCP-Bastion"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"typescript": "^5.3.0",
|
|
45
|
+
"vite": "^5.0.0",
|
|
46
|
+
"vitest": "^1.0.0"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18.0.0"
|
|
50
|
+
}
|
|
51
|
+
}
|