@blekline/mcp-proxy 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/LICENSE ADDED
@@ -0,0 +1,2 @@
1
+ This package is licensed under the GNU Affero General Public License v3.0.
2
+ See https://github.com/Blekline/blekline-oss/blob/main/LICENSE
package/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # @blekline/mcp-proxy
2
+
3
+ MCP proxy router that intercepts downstream tool calls, runs Blekline enforcement (mask/block), then forwards approved calls to a downstream MCP server (e.g. Daytona sandbox).
4
+
5
+ ## Flow
6
+
7
+ ```
8
+ Model → @blekline/mcp-proxy → POST /api/mcp/enforce-tool-call → downstream MCP
9
+ ```
10
+
11
+ ## Env
12
+
13
+ ```bash
14
+ BLEKLINE_WORKSPACE_TOKEN=ws_...
15
+ BLEKLINE_DOWNSTREAM_MCP_COMMAND=... # optional mock or real Daytona MCP
16
+ BLEKLINE_CLIENT_SURFACE=cursor
17
+ ```
18
+
19
+ ## Run
20
+
21
+ ```bash
22
+ pnpm --filter @blekline/mcp-proxy build
23
+ pnpm --filter @blekline/mcp-proxy test
24
+ ```
25
+
26
+ Smoke test: `pnpm demo:mcp-smoke` from repo root.
@@ -0,0 +1,77 @@
1
+ import { spawn } from "node:child_process";
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4
+ /** Mock downstream tools for demo without Daytona API key. */
5
+ export const MOCK_DOWNSTREAM_TOOLS = [
6
+ {
7
+ name: "run_shell",
8
+ description: "Run a shell command in sandbox (mock)",
9
+ inputSchema: {
10
+ type: "object",
11
+ properties: { command: { type: "string" } },
12
+ required: ["command"],
13
+ },
14
+ },
15
+ {
16
+ name: "write_file",
17
+ description: "Write file in sandbox (mock)",
18
+ inputSchema: {
19
+ type: "object",
20
+ properties: { path: { type: "string" }, content: { type: "string" } },
21
+ required: ["path", "content"],
22
+ },
23
+ },
24
+ ];
25
+ export async function listDownstreamTools() {
26
+ if (process.env.BLEKLINE_MCP_PROXY_MOCK === "1") {
27
+ return MOCK_DOWNSTREAM_TOOLS;
28
+ }
29
+ const cmd = process.env.BLEKLINE_DOWNSTREAM_MCP_COMMAND?.trim();
30
+ if (!cmd)
31
+ return MOCK_DOWNSTREAM_TOOLS;
32
+ const parts = cmd.split(",").map((s) => s.trim()).filter(Boolean);
33
+ if (parts.length === 0)
34
+ return MOCK_DOWNSTREAM_TOOLS;
35
+ const [command, ...args] = parts;
36
+ const transport = new StdioClientTransport({ command, args, env: process.env });
37
+ const client = new Client({ name: "blekline-proxy-downstream", version: "0.1.0" }, { capabilities: {} });
38
+ await client.connect(transport);
39
+ const listed = await client.listTools();
40
+ await client.close();
41
+ return listed.tools.map((t) => ({
42
+ name: t.name,
43
+ description: t.description,
44
+ inputSchema: t.inputSchema,
45
+ }));
46
+ }
47
+ export async function callDownstreamTool(name, args) {
48
+ if (process.env.BLEKLINE_MCP_PROXY_MOCK === "1") {
49
+ return { ok: true, mock: true, tool: name, received: args };
50
+ }
51
+ const cmd = process.env.BLEKLINE_DOWNSTREAM_MCP_COMMAND?.trim();
52
+ if (!cmd) {
53
+ return { ok: true, mock: true, tool: name, received: args };
54
+ }
55
+ const parts = cmd.split(",").map((s) => s.trim()).filter(Boolean);
56
+ const [command, ...spawnArgs] = parts;
57
+ const transport = new StdioClientTransport({
58
+ command,
59
+ args: spawnArgs,
60
+ env: process.env,
61
+ });
62
+ const client = new Client({ name: "blekline-proxy-downstream", version: "0.1.0" }, { capabilities: {} });
63
+ await client.connect(transport);
64
+ const result = await client.callTool({ name, arguments: args });
65
+ await client.close();
66
+ return result;
67
+ }
68
+ /** Spawn helper for health checks (unused in hot path). */
69
+ export function spawnDownstreamCheck() {
70
+ const cmd = process.env.BLEKLINE_DOWNSTREAM_MCP_COMMAND?.trim();
71
+ if (!cmd)
72
+ return;
73
+ const parts = cmd.split(",").map((s) => s.trim()).filter(Boolean);
74
+ if (parts.length === 0)
75
+ return;
76
+ spawn(parts[0], parts.slice(1), { stdio: "ignore" });
77
+ }
@@ -0,0 +1,24 @@
1
+ import { scanTextForSecrets } from "@blekline/contracts";
2
+ const DESTRUCTIVE_RE = /\brm\s+-rf\b|\bformat\s+c:\b|\bdrop\s+database\b/i;
3
+ /** Fast local scan of MCP tool arguments (used before cloud enforce-tool-call). */
4
+ export function scanToolArgs(args) {
5
+ let blob = "";
6
+ try {
7
+ blob = JSON.stringify(args);
8
+ }
9
+ catch {
10
+ blob = String(args);
11
+ }
12
+ const findings = [];
13
+ if (DESTRUCTIVE_RE.test(blob)) {
14
+ findings.push({ id: "destructive_command", label: "DESTRUCTIVE", field: "arguments" });
15
+ }
16
+ for (const s of scanTextForSecrets(blob)) {
17
+ findings.push({ id: s.id, label: s.label });
18
+ }
19
+ return {
20
+ findings,
21
+ hasDestructive: DESTRUCTIVE_RE.test(blob),
22
+ secretCount: findings.filter((f) => f.id !== "destructive_command").length,
23
+ };
24
+ }
package/dist/index.js ADDED
@@ -0,0 +1,63 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
4
+ import { callDownstreamTool, listDownstreamTools } from "./downstream/mcp-client.js";
5
+ import { createProxyContext, interceptToolCall } from "./mcp-proxy.js";
6
+ const server = new Server({ name: "blekline-mcp-proxy", version: "0.1.0" }, { capabilities: { tools: {} } });
7
+ let ctx = null;
8
+ function getCtx() {
9
+ if (!ctx)
10
+ ctx = createProxyContext();
11
+ return ctx;
12
+ }
13
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
14
+ const downstream = await listDownstreamTools();
15
+ return {
16
+ tools: downstream.map((t) => ({
17
+ name: t.name,
18
+ description: t.description ?? `Proxied tool (Blekline governed): ${t.name}`,
19
+ inputSchema: t.inputSchema ?? { type: "object", properties: {} },
20
+ })),
21
+ };
22
+ });
23
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
24
+ const toolName = request.params.name;
25
+ const toolArgs = (request.params.arguments ?? {});
26
+ const enforcement = await interceptToolCall(getCtx(), toolName, toolArgs);
27
+ if (!enforcement.ok) {
28
+ return {
29
+ content: [
30
+ {
31
+ type: "text",
32
+ text: JSON.stringify({
33
+ error: enforcement.message,
34
+ action: enforcement.action,
35
+ findings: enforcement.findings,
36
+ }, null, 2),
37
+ },
38
+ ],
39
+ isError: true,
40
+ };
41
+ }
42
+ const downstreamResult = await callDownstreamTool(toolName, enforcement.arguments);
43
+ return {
44
+ content: [
45
+ {
46
+ type: "text",
47
+ text: JSON.stringify({
48
+ bleklineAction: enforcement.action,
49
+ entitiesMasked: enforcement.action === "mask" ? enforcement.entitiesMasked : 0,
50
+ result: downstreamResult,
51
+ }, null, 2),
52
+ },
53
+ ],
54
+ };
55
+ });
56
+ async function main() {
57
+ const transport = new StdioServerTransport();
58
+ await server.connect(transport);
59
+ }
60
+ main().catch((err) => {
61
+ console.error("[blekline-mcp-proxy]", err);
62
+ process.exit(1);
63
+ });
@@ -0,0 +1,70 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { BleklineClient } from "@blekline/client";
3
+ import { enforceToolCallLocally } from "@blekline/contracts";
4
+ function envClientSurface() {
5
+ const v = process.env.BLEKLINE_CLIENT_SURFACE?.trim();
6
+ if (v === "cursor" || v === "claude-desktop" || v === "codex")
7
+ return v;
8
+ return "sdk";
9
+ }
10
+ export function createProxyContext() {
11
+ const token = process.env.BLEKLINE_WORKSPACE_TOKEN?.trim();
12
+ if (!token)
13
+ throw new Error("BLEKLINE_WORKSPACE_TOKEN is required");
14
+ return {
15
+ client: new BleklineClient({
16
+ baseUrl: process.env.BLEKLINE_API_URL?.trim(),
17
+ workspaceToken: token,
18
+ workspaceId: process.env.BLEKLINE_WORKSPACE_ID?.trim(),
19
+ metadata: { clientSurface: envClientSurface() },
20
+ }),
21
+ clientSurface: envClientSurface(),
22
+ useCloudEnforcement: process.env.BLEKLINE_PROXY_LOCAL_ONLY !== "1",
23
+ };
24
+ }
25
+ export async function interceptToolCall(ctx, toolName, toolArgs) {
26
+ const requestId = randomUUID();
27
+ let result = enforceToolCallLocally({ toolName, arguments: toolArgs, requestId });
28
+ if (ctx.useCloudEnforcement) {
29
+ try {
30
+ result = await ctx.client.enforceToolCall({
31
+ toolName,
32
+ arguments: toolArgs,
33
+ platform: "MCP-Proxy",
34
+ clientSurface: ctx.clientSurface,
35
+ });
36
+ }
37
+ catch {
38
+ /* fall back to local result */
39
+ }
40
+ }
41
+ void ctx.client
42
+ .emitEvent({
43
+ kind: "tool_call_enforcement",
44
+ platform: "MCP-Proxy",
45
+ entitiesMasked: result.entitiesMasked,
46
+ riskTier: result.riskTier,
47
+ action: result.action,
48
+ mcpToolName: toolName,
49
+ downstreamServer: process.env.BLEKLINE_MCP_PROXY_MOCK === "1" ? "mock" : "daytona",
50
+ clientSurface: ctx.clientSurface,
51
+ })
52
+ .catch(() => { });
53
+ if (result.action === "block") {
54
+ return {
55
+ ok: false,
56
+ action: "block",
57
+ message: "Blekline policy: block_and_review",
58
+ findings: result.findings,
59
+ };
60
+ }
61
+ if (result.action === "mask") {
62
+ return {
63
+ ok: true,
64
+ action: "mask",
65
+ arguments: result.maskedArguments,
66
+ entitiesMasked: result.entitiesMasked,
67
+ };
68
+ }
69
+ return { ok: true, action: "allow", arguments: toolArgs };
70
+ }
@@ -0,0 +1,21 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { enforceToolCallLocally } from "@blekline/contracts";
4
+ test("blocks destructive shell patterns", () => {
5
+ const result = enforceToolCallLocally({
6
+ toolName: "run_shell",
7
+ arguments: { command: "rm -rf /" },
8
+ requestId: "test-1",
9
+ });
10
+ assert.equal(result.action, "block");
11
+ });
12
+ test("masks AWS key in tool arguments", () => {
13
+ const result = enforceToolCallLocally({
14
+ toolName: "run_shell",
15
+ arguments: { command: "export AWS_KEY=AKIAIOSFODNN7EXAMPLE" },
16
+ requestId: "test-2",
17
+ });
18
+ assert.equal(result.action, "mask");
19
+ assert.ok(result.entitiesMasked > 0);
20
+ assert.ok(JSON.stringify(result.maskedArguments).includes("[AWS_KEY]"));
21
+ });
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@blekline/mcp-proxy",
3
+ "version": "0.1.0",
4
+ "license": "AGPL-3.0-or-later",
5
+ "private": false,
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/Blekline/blekline-oss.git",
9
+ "directory": "packages/mcp-proxy"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "type": "module",
15
+ "bin": {
16
+ "blekline-mcp-proxy": "./dist/index.js"
17
+ },
18
+ "main": "./dist/index.js",
19
+ "dependencies": {
20
+ "@modelcontextprotocol/sdk": "^1.12.1",
21
+ "@blekline/client": "0.1.0",
22
+ "@blekline/contracts": "0.1.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^20",
26
+ "typescript": "^5.7.3"
27
+ },
28
+ "scripts": {
29
+ "build": "tsc",
30
+ "start": "node dist/index.js",
31
+ "typecheck": "tsc --noEmit",
32
+ "test": "node --test dist/mcp-proxy.test.js"
33
+ }
34
+ }
@@ -0,0 +1,88 @@
1
+ import { spawn } from "node:child_process";
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4
+
5
+ export type DownstreamTool = {
6
+ name: string;
7
+ description?: string;
8
+ inputSchema?: Record<string, unknown>;
9
+ };
10
+
11
+ /** Mock downstream tools for demo without Daytona API key. */
12
+ export const MOCK_DOWNSTREAM_TOOLS: DownstreamTool[] = [
13
+ {
14
+ name: "run_shell",
15
+ description: "Run a shell command in sandbox (mock)",
16
+ inputSchema: {
17
+ type: "object",
18
+ properties: { command: { type: "string" } },
19
+ required: ["command"],
20
+ },
21
+ },
22
+ {
23
+ name: "write_file",
24
+ description: "Write file in sandbox (mock)",
25
+ inputSchema: {
26
+ type: "object",
27
+ properties: { path: { type: "string" }, content: { type: "string" } },
28
+ required: ["path", "content"],
29
+ },
30
+ },
31
+ ];
32
+
33
+ export async function listDownstreamTools(): Promise<DownstreamTool[]> {
34
+ if (process.env.BLEKLINE_MCP_PROXY_MOCK === "1") {
35
+ return MOCK_DOWNSTREAM_TOOLS;
36
+ }
37
+
38
+ const cmd = process.env.BLEKLINE_DOWNSTREAM_MCP_COMMAND?.trim();
39
+ if (!cmd) return MOCK_DOWNSTREAM_TOOLS;
40
+
41
+ const parts = cmd.split(",").map((s) => s.trim()).filter(Boolean);
42
+ if (parts.length === 0) return MOCK_DOWNSTREAM_TOOLS;
43
+
44
+ const [command, ...args] = parts;
45
+ const transport = new StdioClientTransport({ command, args, env: process.env as Record<string, string> });
46
+ const client = new Client({ name: "blekline-proxy-downstream", version: "0.1.0" }, { capabilities: {} });
47
+ await client.connect(transport);
48
+ const listed = await client.listTools();
49
+ await client.close();
50
+ return listed.tools.map((t) => ({
51
+ name: t.name,
52
+ description: t.description,
53
+ inputSchema: t.inputSchema as Record<string, unknown>,
54
+ }));
55
+ }
56
+
57
+ export async function callDownstreamTool(name: string, args: Record<string, unknown>): Promise<unknown> {
58
+ if (process.env.BLEKLINE_MCP_PROXY_MOCK === "1") {
59
+ return { ok: true, mock: true, tool: name, received: args };
60
+ }
61
+
62
+ const cmd = process.env.BLEKLINE_DOWNSTREAM_MCP_COMMAND?.trim();
63
+ if (!cmd) {
64
+ return { ok: true, mock: true, tool: name, received: args };
65
+ }
66
+
67
+ const parts = cmd.split(",").map((s) => s.trim()).filter(Boolean);
68
+ const [command, ...spawnArgs] = parts;
69
+ const transport = new StdioClientTransport({
70
+ command,
71
+ args: spawnArgs,
72
+ env: process.env as Record<string, string>,
73
+ });
74
+ const client = new Client({ name: "blekline-proxy-downstream", version: "0.1.0" }, { capabilities: {} });
75
+ await client.connect(transport);
76
+ const result = await client.callTool({ name, arguments: args });
77
+ await client.close();
78
+ return result;
79
+ }
80
+
81
+ /** Spawn helper for health checks (unused in hot path). */
82
+ export function spawnDownstreamCheck(): void {
83
+ const cmd = process.env.BLEKLINE_DOWNSTREAM_MCP_COMMAND?.trim();
84
+ if (!cmd) return;
85
+ const parts = cmd.split(",").map((s) => s.trim()).filter(Boolean);
86
+ if (parts.length === 0) return;
87
+ spawn(parts[0], parts.slice(1), { stdio: "ignore" });
88
+ }
@@ -0,0 +1,35 @@
1
+ import { scanTextForSecrets } from "@blekline/contracts";
2
+ import type { ToolCallFinding } from "@blekline/contracts";
3
+
4
+ const DESTRUCTIVE_RE = /\brm\s+-rf\b|\bformat\s+c:\b|\bdrop\s+database\b/i;
5
+
6
+ export type ScanToolArgsResult = {
7
+ findings: ToolCallFinding[];
8
+ hasDestructive: boolean;
9
+ secretCount: number;
10
+ };
11
+
12
+ /** Fast local scan of MCP tool arguments (used before cloud enforce-tool-call). */
13
+ export function scanToolArgs(args: Record<string, unknown>): ScanToolArgsResult {
14
+ let blob = "";
15
+ try {
16
+ blob = JSON.stringify(args);
17
+ } catch {
18
+ blob = String(args);
19
+ }
20
+
21
+ const findings: ToolCallFinding[] = [];
22
+ if (DESTRUCTIVE_RE.test(blob)) {
23
+ findings.push({ id: "destructive_command", label: "DESTRUCTIVE", field: "arguments" });
24
+ }
25
+
26
+ for (const s of scanTextForSecrets(blob)) {
27
+ findings.push({ id: s.id, label: s.label });
28
+ }
29
+
30
+ return {
31
+ findings,
32
+ hasDestructive: DESTRUCTIVE_RE.test(blob),
33
+ secretCount: findings.filter((f) => f.id !== "destructive_command").length,
34
+ };
35
+ }
package/src/index.ts ADDED
@@ -0,0 +1,82 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
4
+ import { callDownstreamTool, listDownstreamTools } from "./downstream/mcp-client.js";
5
+ import { createProxyContext, interceptToolCall } from "./mcp-proxy.js";
6
+
7
+ const server = new Server(
8
+ { name: "blekline-mcp-proxy", version: "0.1.0" },
9
+ { capabilities: { tools: {} } }
10
+ );
11
+
12
+ let ctx: ReturnType<typeof createProxyContext> | null = null;
13
+
14
+ function getCtx() {
15
+ if (!ctx) ctx = createProxyContext();
16
+ return ctx;
17
+ }
18
+
19
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
20
+ const downstream = await listDownstreamTools();
21
+ return {
22
+ tools: downstream.map((t) => ({
23
+ name: t.name,
24
+ description: t.description ?? `Proxied tool (Blekline governed): ${t.name}`,
25
+ inputSchema: t.inputSchema ?? { type: "object", properties: {} },
26
+ })),
27
+ };
28
+ });
29
+
30
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
31
+ const toolName = request.params.name;
32
+ const toolArgs = (request.params.arguments ?? {}) as Record<string, unknown>;
33
+ const enforcement = await interceptToolCall(getCtx(), toolName, toolArgs);
34
+
35
+ if (!enforcement.ok) {
36
+ return {
37
+ content: [
38
+ {
39
+ type: "text",
40
+ text: JSON.stringify(
41
+ {
42
+ error: enforcement.message,
43
+ action: enforcement.action,
44
+ findings: enforcement.findings,
45
+ },
46
+ null,
47
+ 2
48
+ ),
49
+ },
50
+ ],
51
+ isError: true,
52
+ };
53
+ }
54
+
55
+ const downstreamResult = await callDownstreamTool(toolName, enforcement.arguments);
56
+ return {
57
+ content: [
58
+ {
59
+ type: "text",
60
+ text: JSON.stringify(
61
+ {
62
+ bleklineAction: enforcement.action,
63
+ entitiesMasked: enforcement.action === "mask" ? enforcement.entitiesMasked : 0,
64
+ result: downstreamResult,
65
+ },
66
+ null,
67
+ 2
68
+ ),
69
+ },
70
+ ],
71
+ };
72
+ });
73
+
74
+ async function main() {
75
+ const transport = new StdioServerTransport();
76
+ await server.connect(transport);
77
+ }
78
+
79
+ main().catch((err) => {
80
+ console.error("[blekline-mcp-proxy]", err);
81
+ process.exit(1);
82
+ });
@@ -0,0 +1,23 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { enforceToolCallLocally } from "@blekline/contracts";
4
+
5
+ test("blocks destructive shell patterns", () => {
6
+ const result = enforceToolCallLocally({
7
+ toolName: "run_shell",
8
+ arguments: { command: "rm -rf /" },
9
+ requestId: "test-1",
10
+ });
11
+ assert.equal(result.action, "block");
12
+ });
13
+
14
+ test("masks AWS key in tool arguments", () => {
15
+ const result = enforceToolCallLocally({
16
+ toolName: "run_shell",
17
+ arguments: { command: "export AWS_KEY=AKIAIOSFODNN7EXAMPLE" },
18
+ requestId: "test-2",
19
+ });
20
+ assert.equal(result.action, "mask");
21
+ assert.ok(result.entitiesMasked > 0);
22
+ assert.ok(JSON.stringify(result.maskedArguments).includes("[AWS_KEY]"));
23
+ });
@@ -0,0 +1,90 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { BleklineClient } from "@blekline/client";
3
+ import { enforceToolCallLocally, type ClientSurface } from "@blekline/contracts";
4
+
5
+ export type ProxyEnforcementContext = {
6
+ client: BleklineClient;
7
+ clientSurface: ClientSurface;
8
+ useCloudEnforcement: boolean;
9
+ };
10
+
11
+ export type InterceptResult =
12
+ | { ok: true; action: "allow"; arguments: Record<string, unknown> }
13
+ | { ok: true; action: "mask"; arguments: Record<string, unknown>; entitiesMasked: number }
14
+ | { ok: false; action: "block"; message: string; findings: unknown[] };
15
+
16
+ function envClientSurface(): ClientSurface {
17
+ const v = process.env.BLEKLINE_CLIENT_SURFACE?.trim();
18
+ if (v === "cursor" || v === "claude-desktop" || v === "codex") return v;
19
+ return "sdk";
20
+ }
21
+
22
+ export function createProxyContext(): ProxyEnforcementContext {
23
+ const token = process.env.BLEKLINE_WORKSPACE_TOKEN?.trim();
24
+ if (!token) throw new Error("BLEKLINE_WORKSPACE_TOKEN is required");
25
+ return {
26
+ client: new BleklineClient({
27
+ baseUrl: process.env.BLEKLINE_API_URL?.trim(),
28
+ workspaceToken: token,
29
+ workspaceId: process.env.BLEKLINE_WORKSPACE_ID?.trim(),
30
+ metadata: { clientSurface: envClientSurface() },
31
+ }),
32
+ clientSurface: envClientSurface(),
33
+ useCloudEnforcement: process.env.BLEKLINE_PROXY_LOCAL_ONLY !== "1",
34
+ };
35
+ }
36
+
37
+ export async function interceptToolCall(
38
+ ctx: ProxyEnforcementContext,
39
+ toolName: string,
40
+ toolArgs: Record<string, unknown>
41
+ ): Promise<InterceptResult> {
42
+ const requestId = randomUUID();
43
+ let result = enforceToolCallLocally({ toolName, arguments: toolArgs, requestId });
44
+
45
+ if (ctx.useCloudEnforcement) {
46
+ try {
47
+ result = await ctx.client.enforceToolCall({
48
+ toolName,
49
+ arguments: toolArgs,
50
+ platform: "MCP-Proxy",
51
+ clientSurface: ctx.clientSurface,
52
+ });
53
+ } catch {
54
+ /* fall back to local result */
55
+ }
56
+ }
57
+
58
+ void ctx.client
59
+ .emitEvent({
60
+ kind: "tool_call_enforcement",
61
+ platform: "MCP-Proxy",
62
+ entitiesMasked: result.entitiesMasked,
63
+ riskTier: result.riskTier,
64
+ action: result.action,
65
+ mcpToolName: toolName,
66
+ downstreamServer: process.env.BLEKLINE_MCP_PROXY_MOCK === "1" ? "mock" : "daytona",
67
+ clientSurface: ctx.clientSurface,
68
+ })
69
+ .catch(() => {});
70
+
71
+ if (result.action === "block") {
72
+ return {
73
+ ok: false,
74
+ action: "block",
75
+ message: "Blekline policy: block_and_review",
76
+ findings: result.findings,
77
+ };
78
+ }
79
+
80
+ if (result.action === "mask") {
81
+ return {
82
+ ok: true,
83
+ action: "mask",
84
+ arguments: result.maskedArguments,
85
+ entitiesMasked: result.entitiesMasked,
86
+ };
87
+ }
88
+
89
+ return { ok: true, action: "allow", arguments: toolArgs };
90
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "skipLibCheck": true
10
+ },
11
+ "include": ["src/**/*"]
12
+ }