@blekline/mcp-server 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 +2 -0
- package/README.md +36 -0
- package/dist/index.js +17 -0
- package/dist/server-core.js +144 -0
- package/dist/sse-server.js +42 -0
- package/package.json +34 -0
- package/src/index.ts +20 -0
- package/src/server-core.ts +165 -0
- package/src/sse-server.ts +51 -0
- package/tsconfig.json +12 -0
package/LICENSE
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @blekline/mcp-server
|
|
2
|
+
|
|
3
|
+
Stdio MCP server exposing Blekline governance tools for **Cursor (P0)**, **Claude Desktop (P1)**, and **Codex (P2)**.
|
|
4
|
+
|
|
5
|
+
## Tools
|
|
6
|
+
|
|
7
|
+
| Tool | Purpose |
|
|
8
|
+
|------|---------|
|
|
9
|
+
| `blekline_mask_prompt` | Mask prompt via control plane |
|
|
10
|
+
| `blekline_classify_risk` | Policy simulation |
|
|
11
|
+
| `blekline_emit_event` | Metadata event ingest |
|
|
12
|
+
| `blekline_evaluate_tool_call` | MCP tool allow/mask/block |
|
|
13
|
+
|
|
14
|
+
## Env
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
BLEKLINE_WORKSPACE_TOKEN=ws_...
|
|
18
|
+
BLEKLINE_API_URL=https://app.blekline.com # optional
|
|
19
|
+
BLEKLINE_CLIENT_SURFACE=cursor # cursor | claude-desktop | codex
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Run
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pnpm --filter @blekline/mcp-server build
|
|
26
|
+
node packages/mcp-server/dist/index.js
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### SSE transport (remote deploy)
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
BLEKLINE_MCP_TRANSPORT=sse BLEKLINE_MCP_PORT=3200 node packages/mcp-server/dist/index.js
|
|
33
|
+
# GET http://127.0.0.1:3200/sse
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Generate configs: `pnpm generate:mcp-configs` from repo root.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { createBleklineMcpServer } from "./server-core.js";
|
|
3
|
+
import { startSseServer } from "./sse-server.js";
|
|
4
|
+
async function main() {
|
|
5
|
+
const mode = process.env.BLEKLINE_MCP_TRANSPORT?.trim().toLowerCase() ?? "stdio";
|
|
6
|
+
if (mode === "sse") {
|
|
7
|
+
await startSseServer();
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const server = createBleklineMcpServer();
|
|
11
|
+
const transport = new StdioServerTransport();
|
|
12
|
+
await server.connect(transport);
|
|
13
|
+
}
|
|
14
|
+
main().catch((err) => {
|
|
15
|
+
console.error("[blekline-mcp-server]", err);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { BleklineClient } from "@blekline/client";
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
export 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 createClient() {
|
|
11
|
+
const token = process.env.BLEKLINE_WORKSPACE_TOKEN?.trim();
|
|
12
|
+
if (!token) {
|
|
13
|
+
throw new Error("BLEKLINE_WORKSPACE_TOKEN is required");
|
|
14
|
+
}
|
|
15
|
+
return 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
|
+
}
|
|
22
|
+
export function createBleklineMcpServer() {
|
|
23
|
+
const server = new Server({ name: "blekline-mcp-server", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
24
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
25
|
+
tools: [
|
|
26
|
+
{
|
|
27
|
+
name: "blekline_mask_prompt",
|
|
28
|
+
description: "Mask PII and secrets in a prompt via Blekline control plane before sending to an LLM. Call this before any outbound prompt containing sensitive data.",
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
text: { type: "string", description: "Prompt text to mask" },
|
|
33
|
+
platform: { type: "string", description: "Optional platform label (e.g. Cursor, Claude Desktop)" },
|
|
34
|
+
},
|
|
35
|
+
required: ["text"],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "blekline_classify_risk",
|
|
40
|
+
description: "Simulate Blekline redaction policy on a prompt without masking.",
|
|
41
|
+
inputSchema: {
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: {
|
|
44
|
+
prompt: { type: "string" },
|
|
45
|
+
platform: { type: "string" },
|
|
46
|
+
sourceHost: { type: "string" },
|
|
47
|
+
},
|
|
48
|
+
required: ["prompt"],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "blekline_emit_event",
|
|
53
|
+
description: "Emit metadata-only governance event to Blekline control plane.",
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: "object",
|
|
56
|
+
properties: {
|
|
57
|
+
kind: { type: "string" },
|
|
58
|
+
platform: { type: "string" },
|
|
59
|
+
entitiesMasked: { type: "number" },
|
|
60
|
+
riskTier: { type: "string", enum: ["low", "medium", "high"] },
|
|
61
|
+
action: { type: "string" },
|
|
62
|
+
},
|
|
63
|
+
required: ["kind"],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "blekline_evaluate_tool_call",
|
|
68
|
+
description: "Evaluate MCP tool call arguments against Blekline workspace MCP tool policy (allow/mask/block).",
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: "object",
|
|
71
|
+
properties: {
|
|
72
|
+
toolName: { type: "string" },
|
|
73
|
+
arguments: { type: "object" },
|
|
74
|
+
platform: { type: "string" },
|
|
75
|
+
},
|
|
76
|
+
required: ["toolName", "arguments"],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
}));
|
|
81
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
82
|
+
const client = createClient();
|
|
83
|
+
const name = request.params.name;
|
|
84
|
+
const args = (request.params.arguments ?? {});
|
|
85
|
+
if (name === "blekline_mask_prompt") {
|
|
86
|
+
const text = String(args.text ?? "");
|
|
87
|
+
const platform = args.platform ? String(args.platform) : "MCP";
|
|
88
|
+
const result = await client.mask({ text, platform });
|
|
89
|
+
return {
|
|
90
|
+
content: [
|
|
91
|
+
{
|
|
92
|
+
type: "text",
|
|
93
|
+
text: JSON.stringify({
|
|
94
|
+
maskedText: result.maskedText,
|
|
95
|
+
entitiesMasked: result.entitiesMasked,
|
|
96
|
+
decision: result.decision,
|
|
97
|
+
provider: result.provider,
|
|
98
|
+
requestId: result.requestId,
|
|
99
|
+
}, null, 2),
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (name === "blekline_classify_risk") {
|
|
105
|
+
const prompt = String(args.prompt ?? "");
|
|
106
|
+
const result = await client.simulatePolicy({
|
|
107
|
+
prompt,
|
|
108
|
+
platform: args.platform ? String(args.platform) : undefined,
|
|
109
|
+
sourceHost: args.sourceHost ? String(args.sourceHost) : undefined,
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
content: [{ type: "text", text: JSON.stringify(result.simulation, null, 2) }],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (name === "blekline_emit_event") {
|
|
116
|
+
await client.emitEvent({
|
|
117
|
+
kind: String(args.kind ?? "mcp_event"),
|
|
118
|
+
platform: args.platform ? String(args.platform) : "MCP",
|
|
119
|
+
entitiesMasked: typeof args.entitiesMasked === "number" ? args.entitiesMasked : 0,
|
|
120
|
+
riskTier: args.riskTier === "low" || args.riskTier === "medium" || args.riskTier === "high"
|
|
121
|
+
? args.riskTier
|
|
122
|
+
: undefined,
|
|
123
|
+
action: args.action ? String(args.action) : undefined,
|
|
124
|
+
clientSurface: envClientSurface(),
|
|
125
|
+
});
|
|
126
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true }) }] };
|
|
127
|
+
}
|
|
128
|
+
if (name === "blekline_evaluate_tool_call") {
|
|
129
|
+
const toolName = String(args.toolName ?? "");
|
|
130
|
+
const toolArgs = args.arguments && typeof args.arguments === "object"
|
|
131
|
+
? args.arguments
|
|
132
|
+
: {};
|
|
133
|
+
const result = await client.enforceToolCall({
|
|
134
|
+
toolName,
|
|
135
|
+
arguments: toolArgs,
|
|
136
|
+
platform: args.platform ? String(args.platform) : "MCP",
|
|
137
|
+
clientSurface: envClientSurface(),
|
|
138
|
+
});
|
|
139
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
140
|
+
}
|
|
141
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
142
|
+
});
|
|
143
|
+
return server;
|
|
144
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
3
|
+
import { createBleklineMcpServer } from "./server-core.js";
|
|
4
|
+
const transports = new Map();
|
|
5
|
+
export async function startSseServer() {
|
|
6
|
+
const port = Number(process.env.BLEKLINE_MCP_PORT ?? 3200);
|
|
7
|
+
const host = process.env.BLEKLINE_MCP_HOST ?? "127.0.0.1";
|
|
8
|
+
const httpServer = createServer(async (req, res) => {
|
|
9
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
10
|
+
if (req.method === "GET" && url.pathname === "/sse") {
|
|
11
|
+
const transport = new SSEServerTransport("/message", res);
|
|
12
|
+
transports.set(transport.sessionId, transport);
|
|
13
|
+
transport.onclose = () => transports.delete(transport.sessionId);
|
|
14
|
+
const server = createBleklineMcpServer();
|
|
15
|
+
await server.connect(transport);
|
|
16
|
+
await transport.start();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (req.method === "POST" && url.pathname === "/message") {
|
|
20
|
+
const sessionId = url.searchParams.get("sessionId");
|
|
21
|
+
const transport = sessionId ? transports.get(sessionId) : undefined;
|
|
22
|
+
if (!transport) {
|
|
23
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
24
|
+
res.end(JSON.stringify({ error: "Unknown MCP session" }));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
await transport.handlePostMessage(req, res);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (req.method === "GET" && url.pathname === "/health") {
|
|
31
|
+
json(res, 200, { ok: true, transport: "sse", sessions: transports.size });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
json(res, 404, { error: "Use GET /sse or POST /message?sessionId=..." });
|
|
35
|
+
});
|
|
36
|
+
await new Promise((resolve) => httpServer.listen(port, host, resolve));
|
|
37
|
+
console.error(`[blekline-mcp-server] SSE listening on http://${host}:${port}/sse`);
|
|
38
|
+
}
|
|
39
|
+
function json(res, status, body) {
|
|
40
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
41
|
+
res.end(JSON.stringify(body));
|
|
42
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@blekline/mcp-server",
|
|
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-server"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"bin": {
|
|
16
|
+
"blekline-mcp-server": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
22
|
+
"@blekline/client": "0.1.0",
|
|
23
|
+
"@blekline/contracts": "0.1.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^20",
|
|
27
|
+
"typescript": "^5.7.3"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc",
|
|
31
|
+
"start": "node dist/index.js",
|
|
32
|
+
"typecheck": "tsc --noEmit"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { createBleklineMcpServer } from "./server-core.js";
|
|
3
|
+
import { startSseServer } from "./sse-server.js";
|
|
4
|
+
|
|
5
|
+
async function main() {
|
|
6
|
+
const mode = process.env.BLEKLINE_MCP_TRANSPORT?.trim().toLowerCase() ?? "stdio";
|
|
7
|
+
if (mode === "sse") {
|
|
8
|
+
await startSseServer();
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const server = createBleklineMcpServer();
|
|
13
|
+
const transport = new StdioServerTransport();
|
|
14
|
+
await server.connect(transport);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
main().catch((err) => {
|
|
18
|
+
console.error("[blekline-mcp-server]", err);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { BleklineClient } from "@blekline/client";
|
|
2
|
+
import type { ClientSurface } from "@blekline/contracts";
|
|
3
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
|
|
6
|
+
export function envClientSurface(): ClientSurface {
|
|
7
|
+
const v = process.env.BLEKLINE_CLIENT_SURFACE?.trim();
|
|
8
|
+
if (v === "cursor" || v === "claude-desktop" || v === "codex") return v;
|
|
9
|
+
return "sdk";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createClient(): BleklineClient {
|
|
13
|
+
const token = process.env.BLEKLINE_WORKSPACE_TOKEN?.trim();
|
|
14
|
+
if (!token) {
|
|
15
|
+
throw new Error("BLEKLINE_WORKSPACE_TOKEN is required");
|
|
16
|
+
}
|
|
17
|
+
return new BleklineClient({
|
|
18
|
+
baseUrl: process.env.BLEKLINE_API_URL?.trim(),
|
|
19
|
+
workspaceToken: token,
|
|
20
|
+
workspaceId: process.env.BLEKLINE_WORKSPACE_ID?.trim(),
|
|
21
|
+
metadata: { clientSurface: envClientSurface() },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createBleklineMcpServer(): Server {
|
|
26
|
+
const server = new Server(
|
|
27
|
+
{ name: "blekline-mcp-server", version: "0.1.0" },
|
|
28
|
+
{ capabilities: { tools: {} } }
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
32
|
+
tools: [
|
|
33
|
+
{
|
|
34
|
+
name: "blekline_mask_prompt",
|
|
35
|
+
description:
|
|
36
|
+
"Mask PII and secrets in a prompt via Blekline control plane before sending to an LLM. Call this before any outbound prompt containing sensitive data.",
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
text: { type: "string", description: "Prompt text to mask" },
|
|
41
|
+
platform: { type: "string", description: "Optional platform label (e.g. Cursor, Claude Desktop)" },
|
|
42
|
+
},
|
|
43
|
+
required: ["text"],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "blekline_classify_risk",
|
|
48
|
+
description: "Simulate Blekline redaction policy on a prompt without masking.",
|
|
49
|
+
inputSchema: {
|
|
50
|
+
type: "object",
|
|
51
|
+
properties: {
|
|
52
|
+
prompt: { type: "string" },
|
|
53
|
+
platform: { type: "string" },
|
|
54
|
+
sourceHost: { type: "string" },
|
|
55
|
+
},
|
|
56
|
+
required: ["prompt"],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "blekline_emit_event",
|
|
61
|
+
description: "Emit metadata-only governance event to Blekline control plane.",
|
|
62
|
+
inputSchema: {
|
|
63
|
+
type: "object",
|
|
64
|
+
properties: {
|
|
65
|
+
kind: { type: "string" },
|
|
66
|
+
platform: { type: "string" },
|
|
67
|
+
entitiesMasked: { type: "number" },
|
|
68
|
+
riskTier: { type: "string", enum: ["low", "medium", "high"] },
|
|
69
|
+
action: { type: "string" },
|
|
70
|
+
},
|
|
71
|
+
required: ["kind"],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "blekline_evaluate_tool_call",
|
|
76
|
+
description: "Evaluate MCP tool call arguments against Blekline workspace MCP tool policy (allow/mask/block).",
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: "object",
|
|
79
|
+
properties: {
|
|
80
|
+
toolName: { type: "string" },
|
|
81
|
+
arguments: { type: "object" },
|
|
82
|
+
platform: { type: "string" },
|
|
83
|
+
},
|
|
84
|
+
required: ["toolName", "arguments"],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
91
|
+
const client = createClient();
|
|
92
|
+
const name = request.params.name;
|
|
93
|
+
const args = (request.params.arguments ?? {}) as Record<string, unknown>;
|
|
94
|
+
|
|
95
|
+
if (name === "blekline_mask_prompt") {
|
|
96
|
+
const text = String(args.text ?? "");
|
|
97
|
+
const platform = args.platform ? String(args.platform) : "MCP";
|
|
98
|
+
const result = await client.mask({ text, platform });
|
|
99
|
+
return {
|
|
100
|
+
content: [
|
|
101
|
+
{
|
|
102
|
+
type: "text",
|
|
103
|
+
text: JSON.stringify(
|
|
104
|
+
{
|
|
105
|
+
maskedText: result.maskedText,
|
|
106
|
+
entitiesMasked: result.entitiesMasked,
|
|
107
|
+
decision: result.decision,
|
|
108
|
+
provider: result.provider,
|
|
109
|
+
requestId: result.requestId,
|
|
110
|
+
},
|
|
111
|
+
null,
|
|
112
|
+
2
|
|
113
|
+
),
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (name === "blekline_classify_risk") {
|
|
120
|
+
const prompt = String(args.prompt ?? "");
|
|
121
|
+
const result = await client.simulatePolicy({
|
|
122
|
+
prompt,
|
|
123
|
+
platform: args.platform ? String(args.platform) : undefined,
|
|
124
|
+
sourceHost: args.sourceHost ? String(args.sourceHost) : undefined,
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
content: [{ type: "text", text: JSON.stringify(result.simulation, null, 2) }],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (name === "blekline_emit_event") {
|
|
132
|
+
await client.emitEvent({
|
|
133
|
+
kind: String(args.kind ?? "mcp_event"),
|
|
134
|
+
platform: args.platform ? String(args.platform) : "MCP",
|
|
135
|
+
entitiesMasked: typeof args.entitiesMasked === "number" ? args.entitiesMasked : 0,
|
|
136
|
+
riskTier:
|
|
137
|
+
args.riskTier === "low" || args.riskTier === "medium" || args.riskTier === "high"
|
|
138
|
+
? args.riskTier
|
|
139
|
+
: undefined,
|
|
140
|
+
action: args.action ? String(args.action) : undefined,
|
|
141
|
+
clientSurface: envClientSurface(),
|
|
142
|
+
});
|
|
143
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true }) }] };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (name === "blekline_evaluate_tool_call") {
|
|
147
|
+
const toolName = String(args.toolName ?? "");
|
|
148
|
+
const toolArgs =
|
|
149
|
+
args.arguments && typeof args.arguments === "object"
|
|
150
|
+
? (args.arguments as Record<string, unknown>)
|
|
151
|
+
: {};
|
|
152
|
+
const result = await client.enforceToolCall({
|
|
153
|
+
toolName,
|
|
154
|
+
arguments: toolArgs,
|
|
155
|
+
platform: args.platform ? String(args.platform) : "MCP",
|
|
156
|
+
clientSurface: envClientSurface(),
|
|
157
|
+
});
|
|
158
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return server;
|
|
165
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createServer, type ServerResponse } from "node:http";
|
|
2
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
3
|
+
import { createBleklineMcpServer } from "./server-core.js";
|
|
4
|
+
|
|
5
|
+
const transports = new Map<string, SSEServerTransport>();
|
|
6
|
+
|
|
7
|
+
export async function startSseServer(): Promise<void> {
|
|
8
|
+
const port = Number(process.env.BLEKLINE_MCP_PORT ?? 3200);
|
|
9
|
+
const host = process.env.BLEKLINE_MCP_HOST ?? "127.0.0.1";
|
|
10
|
+
|
|
11
|
+
const httpServer = createServer(async (req, res) => {
|
|
12
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
13
|
+
|
|
14
|
+
if (req.method === "GET" && url.pathname === "/sse") {
|
|
15
|
+
const transport = new SSEServerTransport("/message", res);
|
|
16
|
+
transports.set(transport.sessionId, transport);
|
|
17
|
+
transport.onclose = () => transports.delete(transport.sessionId);
|
|
18
|
+
const server = createBleklineMcpServer();
|
|
19
|
+
await server.connect(transport);
|
|
20
|
+
await transport.start();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (req.method === "POST" && url.pathname === "/message") {
|
|
25
|
+
const sessionId = url.searchParams.get("sessionId");
|
|
26
|
+
const transport = sessionId ? transports.get(sessionId) : undefined;
|
|
27
|
+
if (!transport) {
|
|
28
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
29
|
+
res.end(JSON.stringify({ error: "Unknown MCP session" }));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
await transport.handlePostMessage(req, res);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (req.method === "GET" && url.pathname === "/health") {
|
|
37
|
+
json(res, 200, { ok: true, transport: "sse", sessions: transports.size });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
json(res, 404, { error: "Use GET /sse or POST /message?sessionId=..." });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await new Promise<void>((resolve) => httpServer.listen(port, host, resolve));
|
|
45
|
+
console.error(`[blekline-mcp-server] SSE listening on http://${host}:${port}/sse`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function json(res: ServerResponse, status: number, body: unknown) {
|
|
49
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
50
|
+
res.end(JSON.stringify(body));
|
|
51
|
+
}
|