@scopeblind/protect-mcp 0.1.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/cli.js ADDED
@@ -0,0 +1,396 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // src/gateway.ts
5
+ var import_node_child_process = require("child_process");
6
+ var import_node_crypto2 = require("crypto");
7
+ var import_node_readline = require("readline");
8
+
9
+ // src/policy.ts
10
+ var import_node_crypto = require("crypto");
11
+ var import_node_fs = require("fs");
12
+ function loadPolicy(path) {
13
+ const raw = (0, import_node_fs.readFileSync)(path, "utf-8");
14
+ const parsed = JSON.parse(raw);
15
+ if (!parsed.tools || typeof parsed.tools !== "object") {
16
+ throw new Error(`Invalid policy file: missing "tools" object in ${path}`);
17
+ }
18
+ const policy = { tools: parsed.tools };
19
+ const digest = computePolicyDigest(policy);
20
+ return { policy, digest };
21
+ }
22
+ function computePolicyDigest(policy) {
23
+ const canonical = JSON.stringify(sortKeysDeep(policy));
24
+ return (0, import_node_crypto.createHash)("sha256").update(canonical).digest("hex").slice(0, 16);
25
+ }
26
+ function sortKeysDeep(obj) {
27
+ if (obj === null || typeof obj !== "object") return obj;
28
+ if (Array.isArray(obj)) return obj.map(sortKeysDeep);
29
+ const sorted = {};
30
+ for (const key of Object.keys(obj).sort()) {
31
+ sorted[key] = sortKeysDeep(obj[key]);
32
+ }
33
+ return sorted;
34
+ }
35
+ function getToolPolicy(toolName, policy) {
36
+ if (!policy) {
37
+ return { require: "any" };
38
+ }
39
+ if (policy.tools[toolName]) {
40
+ return policy.tools[toolName];
41
+ }
42
+ if (policy.tools["*"]) {
43
+ return policy.tools["*"];
44
+ }
45
+ return { require: "any" };
46
+ }
47
+ function parseRateLimit(spec) {
48
+ const match = spec.match(/^(\d+)\/(second|minute|hour|day)$/);
49
+ if (!match) {
50
+ throw new Error(`Invalid rate limit format: "${spec}". Expected "N/unit" (e.g. "5/hour")`);
51
+ }
52
+ const count = parseInt(match[1], 10);
53
+ const unit = match[2];
54
+ const windowMs = {
55
+ second: 1e3,
56
+ minute: 6e4,
57
+ hour: 36e5,
58
+ day: 864e5
59
+ };
60
+ return { count, windowMs: windowMs[unit] };
61
+ }
62
+ function checkRateLimit(key, limit, store) {
63
+ const now = Date.now();
64
+ const windowStart = now - limit.windowMs;
65
+ const timestamps = (store.get(key) || []).filter((t) => t > windowStart);
66
+ if (timestamps.length >= limit.count) {
67
+ store.set(key, timestamps);
68
+ return { allowed: false, remaining: 0 };
69
+ }
70
+ timestamps.push(now);
71
+ store.set(key, timestamps);
72
+ return { allowed: true, remaining: limit.count - timestamps.length };
73
+ }
74
+
75
+ // src/gateway.ts
76
+ var ProtectGateway = class {
77
+ child = null;
78
+ config;
79
+ rateLimitStore = /* @__PURE__ */ new Map();
80
+ clientReader = null;
81
+ constructor(config) {
82
+ this.config = config;
83
+ }
84
+ /**
85
+ * Start the gateway: spawn child process and wire up message relay.
86
+ */
87
+ async start() {
88
+ const { command, args, verbose } = this.config;
89
+ if (verbose) {
90
+ this.log(`Starting gateway in ${this.config.enforce ? "enforce" : "observe"} mode`);
91
+ this.log(`Wrapping: ${command} ${args.join(" ")}`);
92
+ if (this.config.policy) {
93
+ this.log(`Policy digest: ${this.config.policyDigest}`);
94
+ }
95
+ }
96
+ this.child = (0, import_node_child_process.spawn)(command, args, {
97
+ stdio: ["pipe", "pipe", "pipe"],
98
+ env: { ...process.env }
99
+ });
100
+ if (!this.child.stdin || !this.child.stdout || !this.child.stderr) {
101
+ throw new Error("Failed to create pipes to child process");
102
+ }
103
+ this.child.stderr.on("data", (data) => {
104
+ process.stderr.write(data);
105
+ });
106
+ const childReader = (0, import_node_readline.createInterface)({ input: this.child.stdout, crlfDelay: Infinity });
107
+ childReader.on("line", (line) => {
108
+ this.handleServerMessage(line);
109
+ });
110
+ this.clientReader = (0, import_node_readline.createInterface)({ input: process.stdin, crlfDelay: Infinity });
111
+ this.clientReader.on("line", (line) => {
112
+ this.handleClientMessage(line);
113
+ });
114
+ this.child.on("exit", (code, signal) => {
115
+ if (this.config.verbose) {
116
+ this.log(`Child process exited (code=${code}, signal=${signal})`);
117
+ }
118
+ process.exit(code ?? 1);
119
+ });
120
+ this.child.on("error", (err) => {
121
+ this.log(`Child process error: ${err.message}`);
122
+ process.exit(1);
123
+ });
124
+ process.on("SIGINT", () => this.stop());
125
+ process.on("SIGTERM", () => this.stop());
126
+ process.stdin.on("end", () => {
127
+ if (this.config.verbose) {
128
+ this.log("Client stdin closed, closing child stdin");
129
+ }
130
+ if (this.child?.stdin?.writable) {
131
+ this.child.stdin.end();
132
+ }
133
+ });
134
+ }
135
+ /**
136
+ * Handle a message from the MCP client (stdin).
137
+ * Intercept tools/call requests; pass through everything else.
138
+ */
139
+ handleClientMessage(raw) {
140
+ const trimmed = raw.trim();
141
+ if (!trimmed) return;
142
+ let message;
143
+ try {
144
+ message = JSON.parse(trimmed);
145
+ } catch {
146
+ this.sendToChild(trimmed);
147
+ return;
148
+ }
149
+ if (message.method === "tools/call" && message.id !== void 0) {
150
+ const result = this.interceptToolCall(message);
151
+ if (result) {
152
+ this.sendToClient(JSON.stringify(result));
153
+ return;
154
+ }
155
+ }
156
+ this.sendToChild(trimmed);
157
+ }
158
+ /**
159
+ * Handle a message from the wrapped MCP server (child stdout).
160
+ * Forward to client (stdout) transparently.
161
+ */
162
+ handleServerMessage(raw) {
163
+ this.sendToClient(raw);
164
+ }
165
+ /**
166
+ * Intercept a tools/call request. Returns a JSON-RPC error response if denied, null if allowed.
167
+ */
168
+ interceptToolCall(request) {
169
+ const toolName = request.params?.name || "unknown";
170
+ const requestId = (0, import_node_crypto2.randomUUID)().slice(0, 12);
171
+ const toolPolicy = getToolPolicy(toolName, this.config.policy);
172
+ if (toolPolicy.block) {
173
+ this.emitDecisionLog({
174
+ tool: toolName,
175
+ decision: "deny",
176
+ reason_code: "policy_block",
177
+ request_id: requestId
178
+ });
179
+ if (this.config.enforce) {
180
+ return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" is blocked by policy`);
181
+ }
182
+ return null;
183
+ }
184
+ if (toolPolicy.rate_limit) {
185
+ try {
186
+ const limit = parseRateLimit(toolPolicy.rate_limit);
187
+ const key = `tool:${toolName}`;
188
+ const { allowed, remaining } = checkRateLimit(key, limit, this.rateLimitStore);
189
+ if (!allowed) {
190
+ this.emitDecisionLog({
191
+ tool: toolName,
192
+ decision: "deny",
193
+ reason_code: "rate_limit_exceeded",
194
+ request_id: requestId,
195
+ rate_limit_remaining: 0
196
+ });
197
+ if (this.config.enforce) {
198
+ return this.makeErrorResponse(
199
+ request.id,
200
+ -32600,
201
+ `Tool "${toolName}" rate limit exceeded (${toolPolicy.rate_limit})`
202
+ );
203
+ }
204
+ return null;
205
+ }
206
+ this.emitDecisionLog({
207
+ tool: toolName,
208
+ decision: "allow",
209
+ reason_code: "policy_allow",
210
+ request_id: requestId,
211
+ rate_limit_remaining: remaining
212
+ });
213
+ } catch {
214
+ this.emitDecisionLog({
215
+ tool: toolName,
216
+ decision: "allow",
217
+ reason_code: "default_allow",
218
+ request_id: requestId
219
+ });
220
+ }
221
+ } else {
222
+ const reasonCode = this.config.enforce ? "policy_allow" : "observe_mode";
223
+ this.emitDecisionLog({
224
+ tool: toolName,
225
+ decision: "allow",
226
+ reason_code: reasonCode,
227
+ request_id: requestId
228
+ });
229
+ }
230
+ return null;
231
+ }
232
+ /**
233
+ * Emit a structured decision log to stderr.
234
+ */
235
+ emitDecisionLog(entry) {
236
+ const log = {
237
+ v: 1,
238
+ tool: entry.tool || "unknown",
239
+ decision: entry.decision || "allow",
240
+ reason_code: entry.reason_code || "default_allow",
241
+ policy_digest: this.config.policyDigest,
242
+ request_id: entry.request_id || (0, import_node_crypto2.randomUUID)().slice(0, 12),
243
+ timestamp: Date.now(),
244
+ mode: this.config.enforce ? "enforce" : "observe",
245
+ ...entry.rate_limit_remaining !== void 0 && { rate_limit_remaining: entry.rate_limit_remaining }
246
+ };
247
+ process.stderr.write(`[PROTECT_MCP] ${JSON.stringify(log)}
248
+ `);
249
+ }
250
+ /**
251
+ * Create a JSON-RPC error response.
252
+ */
253
+ makeErrorResponse(id, code, message) {
254
+ return {
255
+ jsonrpc: "2.0",
256
+ id,
257
+ error: { code, message }
258
+ };
259
+ }
260
+ /**
261
+ * Send a message to the child process (wrapped MCP server).
262
+ */
263
+ sendToChild(message) {
264
+ if (this.child?.stdin?.writable) {
265
+ this.child.stdin.write(message + "\n");
266
+ }
267
+ }
268
+ /**
269
+ * Send a message to the MCP client (stdout).
270
+ */
271
+ sendToClient(message) {
272
+ process.stdout.write(message + "\n");
273
+ }
274
+ /**
275
+ * Log a message to stderr (debug output).
276
+ */
277
+ log(message) {
278
+ process.stderr.write(`[PROTECT_MCP] ${message}
279
+ `);
280
+ }
281
+ /**
282
+ * Stop the gateway: kill child process and exit.
283
+ */
284
+ stop() {
285
+ if (this.clientReader) {
286
+ this.clientReader.close();
287
+ }
288
+ if (this.child) {
289
+ this.child.kill("SIGTERM");
290
+ this.child = null;
291
+ }
292
+ process.exit(0);
293
+ }
294
+ };
295
+
296
+ // src/cli.ts
297
+ function printHelp() {
298
+ process.stderr.write(`
299
+ @scopeblind/protect-mcp \u2014 Security gateway for MCP servers
300
+
301
+ Usage:
302
+ protect-mcp [options] -- <command> [args...]
303
+
304
+ Options:
305
+ --policy <path> Policy JSON file (default: allow-all)
306
+ --slug <slug> ScopeBlind tenant slug (optional)
307
+ --enforce Enable enforcement mode (default: observe-only)
308
+ --verbose Enable debug logging to stderr
309
+ --help Show this help
310
+
311
+ Examples:
312
+ protect-mcp -- node my-server.js
313
+ protect-mcp --policy policy.json --enforce -- node my-server.js
314
+
315
+ `);
316
+ }
317
+ function parseArgs(argv) {
318
+ let policyPath;
319
+ let slug;
320
+ let enforce = false;
321
+ let verbose = false;
322
+ let childCommand = [];
323
+ const separatorIndex = argv.indexOf("--");
324
+ if (separatorIndex === -1) {
325
+ process.stderr.write(
326
+ '[PROTECT_MCP] Error: Missing "--" separator before the command to wrap.\nUsage: protect-mcp [options] -- <command> [args...]\nExample: protect-mcp --policy policy.json -- node my-server.js\n'
327
+ );
328
+ process.exit(1);
329
+ }
330
+ childCommand = argv.slice(separatorIndex + 1);
331
+ if (childCommand.length === 0) {
332
+ process.stderr.write('[PROTECT_MCP] Error: No command specified after "--"\n');
333
+ process.exit(1);
334
+ }
335
+ const options = argv.slice(0, separatorIndex);
336
+ for (let i = 0; i < options.length; i++) {
337
+ const arg = options[i];
338
+ if (arg === "--help" || arg === "-h") {
339
+ printHelp();
340
+ process.exit(0);
341
+ } else if (arg === "--policy" && i + 1 < options.length) {
342
+ policyPath = options[++i];
343
+ } else if (arg === "--slug" && i + 1 < options.length) {
344
+ slug = options[++i];
345
+ } else if (arg === "--enforce") {
346
+ enforce = true;
347
+ } else if (arg === "--verbose" || arg === "-v") {
348
+ verbose = true;
349
+ } else {
350
+ process.stderr.write(`[PROTECT_MCP] Warning: Unknown option "${arg}"
351
+ `);
352
+ }
353
+ }
354
+ return { policyPath, slug, enforce, verbose, childCommand };
355
+ }
356
+ async function main() {
357
+ const args = process.argv.slice(2);
358
+ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
359
+ printHelp();
360
+ process.exit(0);
361
+ }
362
+ const { policyPath, slug, enforce, verbose, childCommand } = parseArgs(args);
363
+ let policy = null;
364
+ let policyDigest = "none";
365
+ if (policyPath) {
366
+ try {
367
+ const loaded = loadPolicy(policyPath);
368
+ policy = loaded.policy;
369
+ policyDigest = loaded.digest;
370
+ if (verbose) {
371
+ process.stderr.write(`[PROTECT_MCP] Loaded policy from ${policyPath} (digest: ${policyDigest})
372
+ `);
373
+ }
374
+ } catch (err) {
375
+ process.stderr.write(`[PROTECT_MCP] Error loading policy: ${err instanceof Error ? err.message : err}
376
+ `);
377
+ process.exit(1);
378
+ }
379
+ }
380
+ const config = {
381
+ command: childCommand[0],
382
+ args: childCommand.slice(1),
383
+ policy,
384
+ policyDigest,
385
+ slug,
386
+ enforce,
387
+ verbose
388
+ };
389
+ const gateway = new ProtectGateway(config);
390
+ await gateway.start();
391
+ }
392
+ main().catch((err) => {
393
+ process.stderr.write(`[PROTECT_MCP] Fatal error: ${err instanceof Error ? err.message : err}
394
+ `);
395
+ process.exit(1);
396
+ });
package/dist/cli.mjs ADDED
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ProtectGateway,
4
+ loadPolicy
5
+ } from "./chunk-L3QWKSGY.mjs";
6
+
7
+ // src/cli.ts
8
+ function printHelp() {
9
+ process.stderr.write(`
10
+ @scopeblind/protect-mcp \u2014 Security gateway for MCP servers
11
+
12
+ Usage:
13
+ protect-mcp [options] -- <command> [args...]
14
+
15
+ Options:
16
+ --policy <path> Policy JSON file (default: allow-all)
17
+ --slug <slug> ScopeBlind tenant slug (optional)
18
+ --enforce Enable enforcement mode (default: observe-only)
19
+ --verbose Enable debug logging to stderr
20
+ --help Show this help
21
+
22
+ Examples:
23
+ protect-mcp -- node my-server.js
24
+ protect-mcp --policy policy.json --enforce -- node my-server.js
25
+
26
+ `);
27
+ }
28
+ function parseArgs(argv) {
29
+ let policyPath;
30
+ let slug;
31
+ let enforce = false;
32
+ let verbose = false;
33
+ let childCommand = [];
34
+ const separatorIndex = argv.indexOf("--");
35
+ if (separatorIndex === -1) {
36
+ process.stderr.write(
37
+ '[PROTECT_MCP] Error: Missing "--" separator before the command to wrap.\nUsage: protect-mcp [options] -- <command> [args...]\nExample: protect-mcp --policy policy.json -- node my-server.js\n'
38
+ );
39
+ process.exit(1);
40
+ }
41
+ childCommand = argv.slice(separatorIndex + 1);
42
+ if (childCommand.length === 0) {
43
+ process.stderr.write('[PROTECT_MCP] Error: No command specified after "--"\n');
44
+ process.exit(1);
45
+ }
46
+ const options = argv.slice(0, separatorIndex);
47
+ for (let i = 0; i < options.length; i++) {
48
+ const arg = options[i];
49
+ if (arg === "--help" || arg === "-h") {
50
+ printHelp();
51
+ process.exit(0);
52
+ } else if (arg === "--policy" && i + 1 < options.length) {
53
+ policyPath = options[++i];
54
+ } else if (arg === "--slug" && i + 1 < options.length) {
55
+ slug = options[++i];
56
+ } else if (arg === "--enforce") {
57
+ enforce = true;
58
+ } else if (arg === "--verbose" || arg === "-v") {
59
+ verbose = true;
60
+ } else {
61
+ process.stderr.write(`[PROTECT_MCP] Warning: Unknown option "${arg}"
62
+ `);
63
+ }
64
+ }
65
+ return { policyPath, slug, enforce, verbose, childCommand };
66
+ }
67
+ async function main() {
68
+ const args = process.argv.slice(2);
69
+ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
70
+ printHelp();
71
+ process.exit(0);
72
+ }
73
+ const { policyPath, slug, enforce, verbose, childCommand } = parseArgs(args);
74
+ let policy = null;
75
+ let policyDigest = "none";
76
+ if (policyPath) {
77
+ try {
78
+ const loaded = loadPolicy(policyPath);
79
+ policy = loaded.policy;
80
+ policyDigest = loaded.digest;
81
+ if (verbose) {
82
+ process.stderr.write(`[PROTECT_MCP] Loaded policy from ${policyPath} (digest: ${policyDigest})
83
+ `);
84
+ }
85
+ } catch (err) {
86
+ process.stderr.write(`[PROTECT_MCP] Error loading policy: ${err instanceof Error ? err.message : err}
87
+ `);
88
+ process.exit(1);
89
+ }
90
+ }
91
+ const config = {
92
+ command: childCommand[0],
93
+ args: childCommand.slice(1),
94
+ policy,
95
+ policyDigest,
96
+ slug,
97
+ enforce,
98
+ verbose
99
+ };
100
+ const gateway = new ProtectGateway(config);
101
+ await gateway.start();
102
+ }
103
+ main().catch((err) => {
104
+ process.stderr.write(`[PROTECT_MCP] Fatal error: ${err instanceof Error ? err.message : err}
105
+ `);
106
+ process.exit(1);
107
+ });
@@ -0,0 +1,157 @@
1
+ interface ProtectPolicy {
2
+ tools: Record<string, ToolPolicy>;
3
+ }
4
+ interface ToolPolicy {
5
+ /**
6
+ * Identity requirement for this tool.
7
+ * 'gateway' = must pass through this gateway. 'any' = no restriction. 'none' = no identity needed.
8
+ *
9
+ * NOTE: v1 does not enforce this field — it is metadata only, logged in decision entries
10
+ * for policy documentation purposes. Per-request identity enforcement requires the
11
+ * SSE transport mode planned for v2.
12
+ */
13
+ require?: 'gateway' | 'any' | 'none';
14
+ /** Rate limit spec, e.g. "5/hour", "100/day", "10/minute" */
15
+ rate_limit?: string;
16
+ /** Explicitly block this tool */
17
+ block?: boolean;
18
+ }
19
+ interface RateLimit {
20
+ count: number;
21
+ windowMs: number;
22
+ }
23
+ interface JsonRpcRequest {
24
+ jsonrpc: '2.0';
25
+ id: string | number;
26
+ method: string;
27
+ params?: Record<string, unknown>;
28
+ }
29
+ interface JsonRpcResponse {
30
+ jsonrpc: '2.0';
31
+ id: string | number;
32
+ result?: unknown;
33
+ error?: {
34
+ code: number;
35
+ message: string;
36
+ data?: unknown;
37
+ };
38
+ }
39
+ interface DecisionLog {
40
+ /** Schema version */
41
+ v: 1;
42
+ /** Tool name that was called */
43
+ tool: string;
44
+ /** Decision: allow or deny */
45
+ decision: 'allow' | 'deny';
46
+ /** Why this decision was made */
47
+ reason_code: 'policy_allow' | 'policy_block' | 'rate_limit_exceeded' | 'observe_mode' | 'default_allow';
48
+ /** SHA-256 digest of the canonicalized policy file */
49
+ policy_digest: string;
50
+ /** Unique request identifier */
51
+ request_id: string;
52
+ /** Unix timestamp (ms) */
53
+ timestamp: number;
54
+ /** Remaining rate limit budget (if rate limit is configured) */
55
+ rate_limit_remaining?: number;
56
+ /** Operating mode */
57
+ mode: 'observe' | 'enforce';
58
+ }
59
+ interface ProtectConfig {
60
+ /** Command to spawn (first element of child process) */
61
+ command: string;
62
+ /** Arguments for the child process */
63
+ args: string[];
64
+ /** Loaded policy (or null for allow-all) */
65
+ policy: ProtectPolicy | null;
66
+ /** Computed policy digest */
67
+ policyDigest: string;
68
+ /** ScopeBlind tenant slug (optional, for future API integration) */
69
+ slug?: string;
70
+ /** Whether to enforce policy (default: false = observe mode) */
71
+ enforce?: boolean;
72
+ /** Verbose debug logging to stderr */
73
+ verbose?: boolean;
74
+ }
75
+
76
+ /**
77
+ * ProtectGateway — stdio MITM proxy for MCP servers.
78
+ *
79
+ * Sits between an MCP client (stdin/stdout) and a wrapped MCP server (child process).
80
+ * Intercepts `tools/call` requests for policy enforcement and decision logging.
81
+ * Passes through all other JSON-RPC messages transparently.
82
+ */
83
+ declare class ProtectGateway {
84
+ private child;
85
+ private config;
86
+ private rateLimitStore;
87
+ private clientReader;
88
+ constructor(config: ProtectConfig);
89
+ /**
90
+ * Start the gateway: spawn child process and wire up message relay.
91
+ */
92
+ start(): Promise<void>;
93
+ /**
94
+ * Handle a message from the MCP client (stdin).
95
+ * Intercept tools/call requests; pass through everything else.
96
+ */
97
+ private handleClientMessage;
98
+ /**
99
+ * Handle a message from the wrapped MCP server (child stdout).
100
+ * Forward to client (stdout) transparently.
101
+ */
102
+ private handleServerMessage;
103
+ /**
104
+ * Intercept a tools/call request. Returns a JSON-RPC error response if denied, null if allowed.
105
+ */
106
+ private interceptToolCall;
107
+ /**
108
+ * Emit a structured decision log to stderr.
109
+ */
110
+ private emitDecisionLog;
111
+ /**
112
+ * Create a JSON-RPC error response.
113
+ */
114
+ private makeErrorResponse;
115
+ /**
116
+ * Send a message to the child process (wrapped MCP server).
117
+ */
118
+ private sendToChild;
119
+ /**
120
+ * Send a message to the MCP client (stdout).
121
+ */
122
+ private sendToClient;
123
+ /**
124
+ * Log a message to stderr (debug output).
125
+ */
126
+ private log;
127
+ /**
128
+ * Stop the gateway: kill child process and exit.
129
+ */
130
+ stop(): void;
131
+ }
132
+
133
+ /**
134
+ * Load and validate a policy file. Returns the policy and its digest.
135
+ */
136
+ declare function loadPolicy(path: string): {
137
+ policy: ProtectPolicy;
138
+ digest: string;
139
+ };
140
+ /**
141
+ * Get the policy for a specific tool. Falls back to "*" wildcard, then default-allow.
142
+ */
143
+ declare function getToolPolicy(toolName: string, policy: ProtectPolicy | null): ToolPolicy;
144
+ /**
145
+ * Parse a rate limit spec like "5/hour", "100/day", "10/minute".
146
+ */
147
+ declare function parseRateLimit(spec: string): RateLimit;
148
+ /**
149
+ * In-memory sliding window rate limiter.
150
+ * Returns { allowed, remaining } based on recent invocations.
151
+ */
152
+ declare function checkRateLimit(key: string, limit: RateLimit, store: Map<string, number[]>): {
153
+ allowed: boolean;
154
+ remaining: number;
155
+ };
156
+
157
+ export { type DecisionLog, type JsonRpcRequest, type JsonRpcResponse, type ProtectConfig, ProtectGateway, type ProtectPolicy, type RateLimit, type ToolPolicy, checkRateLimit, getToolPolicy, loadPolicy, parseRateLimit };