protect-mcp 0.4.0 → 0.4.1

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.
@@ -814,6 +814,9 @@ var ProtectGateway = class {
814
814
  approvalNonce = randomBytes(16).toString("hex");
815
815
  currentTier = "unknown";
816
816
  admissionResult = null;
817
+ /** HTTP transport mode: pending response resolvers keyed by JSON-RPC id */
818
+ pendingResponses = /* @__PURE__ */ new Map();
819
+ httpMode = false;
817
820
  constructor(config) {
818
821
  this.config = config;
819
822
  this.logFilePath = join3(process.cwd(), LOG_FILE2);
@@ -1133,8 +1136,108 @@ var ProtectGateway = class {
1133
1136
  if (this.child?.stdin?.writable) this.child.stdin.write(message + "\n");
1134
1137
  }
1135
1138
  sendToClient(message) {
1139
+ if (this.httpMode) {
1140
+ try {
1141
+ const parsed = JSON.parse(message);
1142
+ if (parsed.id !== void 0 && parsed.id !== null) {
1143
+ const pending = this.pendingResponses.get(parsed.id);
1144
+ if (pending) {
1145
+ clearTimeout(pending.timeout);
1146
+ this.pendingResponses.delete(parsed.id);
1147
+ pending.resolve(message);
1148
+ return;
1149
+ }
1150
+ }
1151
+ } catch {
1152
+ }
1153
+ }
1136
1154
  process.stdout.write(message + "\n");
1137
1155
  }
1156
+ /**
1157
+ * Enable HTTP transport mode.
1158
+ * In this mode, sendToClient resolves pending promises instead of
1159
+ * writing to stdout, and start() skips stdin reading.
1160
+ */
1161
+ enableHttpMode() {
1162
+ this.httpMode = true;
1163
+ }
1164
+ /**
1165
+ * Start in HTTP mode — spawns child process but does NOT read from
1166
+ * process.stdin. Requests come in via processRequest() instead.
1167
+ */
1168
+ async startForHttp() {
1169
+ this.httpMode = true;
1170
+ const { command, args, verbose } = this.config;
1171
+ const mode = this.config.enforce ? "enforce" : "shadow";
1172
+ if (verbose) {
1173
+ this.log(`Starting gateway in ${mode} mode (HTTP transport)`);
1174
+ this.log(`Wrapping: ${command} ${args.join(" ")}`);
1175
+ }
1176
+ this.log(`Approval nonce: ${this.approvalNonce}`);
1177
+ const childEnv = { ...process.env };
1178
+ if (this.config.credentials) {
1179
+ for (const [label, credConfig] of Object.entries(this.config.credentials)) {
1180
+ if (credConfig.inject === "env" && credConfig.name && credConfig.value_env) {
1181
+ const envValue = process.env[credConfig.value_env];
1182
+ if (envValue) {
1183
+ childEnv[credConfig.name] = envValue;
1184
+ if (verbose) this.log(`Credential "${label}": injected as env var "${credConfig.name}"`);
1185
+ }
1186
+ }
1187
+ }
1188
+ }
1189
+ this.child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"], env: childEnv });
1190
+ if (!this.child.stdin || !this.child.stdout || !this.child.stderr) {
1191
+ throw new Error("Failed to create pipes to child process");
1192
+ }
1193
+ this.child.stderr.on("data", (data) => {
1194
+ process.stderr.write(data);
1195
+ });
1196
+ const childReader = createInterface({ input: this.child.stdout, crlfDelay: Infinity });
1197
+ childReader.on("line", (line) => {
1198
+ this.handleServerMessage(line);
1199
+ });
1200
+ this.child.on("exit", (code, signal) => {
1201
+ if (verbose) this.log(`Child process exited (code=${code}, signal=${signal})`);
1202
+ this.evidenceStore.save();
1203
+ });
1204
+ this.child.on("error", (err) => {
1205
+ this.log(`Child process error: ${err.message}`);
1206
+ });
1207
+ }
1208
+ /**
1209
+ * Process a JSON-RPC request programmatically (for HTTP transport).
1210
+ * Returns a promise that resolves with the JSON-RPC response string.
1211
+ */
1212
+ async processRequest(jsonRpc) {
1213
+ const REQUEST_TIMEOUT_MS = 3e4;
1214
+ if (jsonRpc.method === "tools/call" && jsonRpc.id !== void 0) {
1215
+ const blocked = await this.interceptToolCall(jsonRpc);
1216
+ if (blocked) {
1217
+ return JSON.stringify(blocked);
1218
+ }
1219
+ }
1220
+ return new Promise((resolve, reject) => {
1221
+ const id = jsonRpc.id;
1222
+ if (id === void 0 || id === null) {
1223
+ const modified2 = this.injectParamsCredentials(jsonRpc);
1224
+ this.sendToChild(JSON.stringify(modified2));
1225
+ resolve(JSON.stringify({ jsonrpc: "2.0", result: {}, id: null }));
1226
+ return;
1227
+ }
1228
+ const timeout = setTimeout(() => {
1229
+ this.pendingResponses.delete(id);
1230
+ resolve(JSON.stringify({
1231
+ jsonrpc: "2.0",
1232
+ error: { code: -32e3, message: "Request timeout (30s)" },
1233
+ id
1234
+ }));
1235
+ }, REQUEST_TIMEOUT_MS);
1236
+ this.pendingResponses.set(id, { resolve, timeout });
1237
+ const modified = this.injectParamsCredentials(jsonRpc);
1238
+ this.sendToChild(JSON.stringify(modified));
1239
+ });
1240
+ }
1138
1241
  log(message) {
1139
1242
  process.stderr.write(`[PROTECT_MCP] ${message}
1140
1243
  `);
@@ -3,7 +3,7 @@ import {
3
3
  getToolPolicy,
4
4
  meetsMinTier,
5
5
  parseRateLimit
6
- } from "./chunk-XMZWJOC3.mjs";
6
+ } from "./chunk-7HBHIKLN.mjs";
7
7
 
8
8
  // src/simulate.ts
9
9
  import { readFileSync } from "fs";
package/dist/cli.js CHANGED
@@ -883,6 +883,9 @@ var init_gateway = __esm({
883
883
  approvalNonce = (0, import_node_crypto2.randomBytes)(16).toString("hex");
884
884
  currentTier = "unknown";
885
885
  admissionResult = null;
886
+ /** HTTP transport mode: pending response resolvers keyed by JSON-RPC id */
887
+ pendingResponses = /* @__PURE__ */ new Map();
888
+ httpMode = false;
886
889
  constructor(config) {
887
890
  this.config = config;
888
891
  this.logFilePath = (0, import_node_path3.join)(process.cwd(), LOG_FILE2);
@@ -1202,8 +1205,108 @@ var init_gateway = __esm({
1202
1205
  if (this.child?.stdin?.writable) this.child.stdin.write(message + "\n");
1203
1206
  }
1204
1207
  sendToClient(message) {
1208
+ if (this.httpMode) {
1209
+ try {
1210
+ const parsed = JSON.parse(message);
1211
+ if (parsed.id !== void 0 && parsed.id !== null) {
1212
+ const pending = this.pendingResponses.get(parsed.id);
1213
+ if (pending) {
1214
+ clearTimeout(pending.timeout);
1215
+ this.pendingResponses.delete(parsed.id);
1216
+ pending.resolve(message);
1217
+ return;
1218
+ }
1219
+ }
1220
+ } catch {
1221
+ }
1222
+ }
1205
1223
  process.stdout.write(message + "\n");
1206
1224
  }
1225
+ /**
1226
+ * Enable HTTP transport mode.
1227
+ * In this mode, sendToClient resolves pending promises instead of
1228
+ * writing to stdout, and start() skips stdin reading.
1229
+ */
1230
+ enableHttpMode() {
1231
+ this.httpMode = true;
1232
+ }
1233
+ /**
1234
+ * Start in HTTP mode — spawns child process but does NOT read from
1235
+ * process.stdin. Requests come in via processRequest() instead.
1236
+ */
1237
+ async startForHttp() {
1238
+ this.httpMode = true;
1239
+ const { command, args, verbose } = this.config;
1240
+ const mode = this.config.enforce ? "enforce" : "shadow";
1241
+ if (verbose) {
1242
+ this.log(`Starting gateway in ${mode} mode (HTTP transport)`);
1243
+ this.log(`Wrapping: ${command} ${args.join(" ")}`);
1244
+ }
1245
+ this.log(`Approval nonce: ${this.approvalNonce}`);
1246
+ const childEnv = { ...process.env };
1247
+ if (this.config.credentials) {
1248
+ for (const [label, credConfig] of Object.entries(this.config.credentials)) {
1249
+ if (credConfig.inject === "env" && credConfig.name && credConfig.value_env) {
1250
+ const envValue = process.env[credConfig.value_env];
1251
+ if (envValue) {
1252
+ childEnv[credConfig.name] = envValue;
1253
+ if (verbose) this.log(`Credential "${label}": injected as env var "${credConfig.name}"`);
1254
+ }
1255
+ }
1256
+ }
1257
+ }
1258
+ this.child = (0, import_node_child_process.spawn)(command, args, { stdio: ["pipe", "pipe", "pipe"], env: childEnv });
1259
+ if (!this.child.stdin || !this.child.stdout || !this.child.stderr) {
1260
+ throw new Error("Failed to create pipes to child process");
1261
+ }
1262
+ this.child.stderr.on("data", (data) => {
1263
+ process.stderr.write(data);
1264
+ });
1265
+ const childReader = (0, import_node_readline.createInterface)({ input: this.child.stdout, crlfDelay: Infinity });
1266
+ childReader.on("line", (line) => {
1267
+ this.handleServerMessage(line);
1268
+ });
1269
+ this.child.on("exit", (code, signal) => {
1270
+ if (verbose) this.log(`Child process exited (code=${code}, signal=${signal})`);
1271
+ this.evidenceStore.save();
1272
+ });
1273
+ this.child.on("error", (err) => {
1274
+ this.log(`Child process error: ${err.message}`);
1275
+ });
1276
+ }
1277
+ /**
1278
+ * Process a JSON-RPC request programmatically (for HTTP transport).
1279
+ * Returns a promise that resolves with the JSON-RPC response string.
1280
+ */
1281
+ async processRequest(jsonRpc) {
1282
+ const REQUEST_TIMEOUT_MS = 3e4;
1283
+ if (jsonRpc.method === "tools/call" && jsonRpc.id !== void 0) {
1284
+ const blocked = await this.interceptToolCall(jsonRpc);
1285
+ if (blocked) {
1286
+ return JSON.stringify(blocked);
1287
+ }
1288
+ }
1289
+ return new Promise((resolve, reject) => {
1290
+ const id = jsonRpc.id;
1291
+ if (id === void 0 || id === null) {
1292
+ const modified2 = this.injectParamsCredentials(jsonRpc);
1293
+ this.sendToChild(JSON.stringify(modified2));
1294
+ resolve(JSON.stringify({ jsonrpc: "2.0", result: {}, id: null }));
1295
+ return;
1296
+ }
1297
+ const timeout = setTimeout(() => {
1298
+ this.pendingResponses.delete(id);
1299
+ resolve(JSON.stringify({
1300
+ jsonrpc: "2.0",
1301
+ error: { code: -32e3, message: "Request timeout (30s)" },
1302
+ id
1303
+ }));
1304
+ }, REQUEST_TIMEOUT_MS);
1305
+ this.pendingResponses.set(id, { resolve, timeout });
1306
+ const modified = this.injectParamsCredentials(jsonRpc);
1307
+ this.sendToChild(JSON.stringify(modified));
1308
+ });
1309
+ }
1207
1310
  log(message) {
1208
1311
  process.stderr.write(`[PROTECT_MCP] ${message}
1209
1312
  `);
@@ -3893,15 +3996,22 @@ var http_transport_exports = {};
3893
3996
  __export(http_transport_exports, {
3894
3997
  startHttpTransport: () => startHttpTransport
3895
3998
  });
3896
- function startHttpTransport(options) {
3999
+ async function startHttpTransport(options) {
3897
4000
  const { port, config, serverCommand } = options;
3898
4001
  const sseClients = /* @__PURE__ */ new Set();
3899
- const gateway = new ProtectGateway(config);
4002
+ const httpConfig = {
4003
+ ...config,
4004
+ command: serverCommand[0],
4005
+ args: serverCommand.slice(1)
4006
+ };
4007
+ const gateway = new ProtectGateway(httpConfig);
4008
+ await gateway.startForHttp();
3900
4009
  const server = (0, import_node_http2.createServer)(async (req, res) => {
3901
4010
  const origin = req.headers.origin || "*";
3902
4011
  res.setHeader("Access-Control-Allow-Origin", origin);
3903
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
3904
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
4012
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
4013
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id");
4014
+ res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
3905
4015
  res.setHeader("Access-Control-Allow-Credentials", "true");
3906
4016
  if (req.method === "OPTIONS") {
3907
4017
  res.writeHead(204);
@@ -3914,9 +4024,10 @@ function startHttpTransport(options) {
3914
4024
  res.end(JSON.stringify({
3915
4025
  status: "ok",
3916
4026
  server: "protect-mcp",
3917
- version: "0.3.3",
3918
- transport: "http",
3919
- mode: config.policy ? config.enforce ? "enforce" : "shadow" : "shadow"
4027
+ version: "0.4.0",
4028
+ transport: "streamable-http",
4029
+ mode: config.policy ? config.enforce ? "enforce" : "shadow" : "shadow",
4030
+ wrapping: serverCommand.join(" ")
3920
4031
  }));
3921
4032
  return;
3922
4033
  }
@@ -3942,28 +4053,35 @@ function startHttpTransport(options) {
3942
4053
  try {
3943
4054
  const jsonRpc = JSON.parse(body);
3944
4055
  const acceptSSE = (req.headers.accept || "").includes("text/event-stream");
4056
+ const responseStr = await gateway.processRequest(jsonRpc);
4057
+ const response = JSON.parse(responseStr);
3945
4058
  if (acceptSSE) {
3946
4059
  res.writeHead(200, {
3947
4060
  "Content-Type": "text/event-stream",
3948
4061
  "Cache-Control": "no-cache"
3949
4062
  });
3950
- const response = await processJsonRpc(gateway, jsonRpc, config, serverCommand);
3951
4063
  res.write(`data: ${JSON.stringify(response)}
3952
4064
 
3953
4065
  `);
3954
4066
  res.end();
3955
4067
  } else {
3956
- const response = await processJsonRpc(gateway, jsonRpc, config, serverCommand);
3957
4068
  res.writeHead(200, { "Content-Type": "application/json" });
3958
4069
  res.end(JSON.stringify(response));
3959
4070
  }
3960
- for (const client of sseClients) {
3961
- try {
3962
- client.write(`data: ${JSON.stringify({ type: "decision", request: jsonRpc, timestamp: (/* @__PURE__ */ new Date()).toISOString() })}
4071
+ if (jsonRpc.method === "tools/call") {
4072
+ const event = {
4073
+ type: "decision",
4074
+ tool: jsonRpc.params?.name,
4075
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4076
+ };
4077
+ for (const client of sseClients) {
4078
+ try {
4079
+ client.write(`data: ${JSON.stringify(event)}
3963
4080
 
3964
4081
  `);
3965
- } catch {
3966
- sseClients.delete(client);
4082
+ } catch {
4083
+ sseClients.delete(client);
4084
+ }
3967
4085
  }
3968
4086
  }
3969
4087
  } catch (err) {
@@ -3977,24 +4095,43 @@ function startHttpTransport(options) {
3977
4095
  });
3978
4096
  return;
3979
4097
  }
4098
+ if (url.pathname === "/mcp" && req.method === "DELETE") {
4099
+ res.writeHead(200, { "Content-Type": "application/json" });
4100
+ res.end(JSON.stringify({ status: "session_closed" }));
4101
+ return;
4102
+ }
3980
4103
  res.writeHead(404, { "Content-Type": "application/json" });
3981
- res.end(JSON.stringify({ error: "not_found", endpoints: ["/mcp", "/mcp/sse", "/health"] }));
4104
+ res.end(JSON.stringify({
4105
+ error: "not_found",
4106
+ endpoints: [
4107
+ "POST /mcp \u2014 JSON-RPC endpoint (Streamable HTTP)",
4108
+ "GET /mcp/sse \u2014 Server-Sent Events stream",
4109
+ "GET /health \u2014 Health check",
4110
+ "DELETE /mcp \u2014 Close session"
4111
+ ]
4112
+ }));
3982
4113
  });
3983
4114
  server.listen(port, () => {
3984
4115
  process.stderr.write(`
3985
- protect-mcp HTTP server listening on port ${port}
4116
+ [PROTECT_MCP] HTTP transport listening on http://0.0.0.0:${port}
3986
4117
  `);
3987
- process.stderr.write(` POST /mcp \u2014 JSON-RPC endpoint (Streamable HTTP)
4118
+ process.stderr.write(` POST /mcp \u2014 JSON-RPC (Streamable HTTP)
3988
4119
  `);
3989
- process.stderr.write(` GET /mcp/sse \u2014 Server-Sent Events stream
4120
+ process.stderr.write(` GET /mcp/sse \u2014 Server-Sent Events
3990
4121
  `);
3991
- process.stderr.write(` GET /health \u2014 Health check
4122
+ process.stderr.write(` GET /health \u2014 Health check
4123
+ `);
4124
+ process.stderr.write(` DELETE /mcp \u2014 Close session
4125
+ `);
4126
+ process.stderr.write(`
4127
+ Wrapping: ${serverCommand.join(" ")}
4128
+ `);
4129
+ process.stderr.write(` Mode: ${config.enforce ? "enforce" : "shadow"}
3992
4130
 
3993
4131
  `);
3994
4132
  });
3995
- gateway.start(serverCommand[0], serverCommand.slice(1));
3996
- process.on("SIGINT", () => {
3997
- process.stderr.write("\nShutting down HTTP transport...\n");
4133
+ const shutdown = () => {
4134
+ process.stderr.write("\n[PROTECT_MCP] Shutting down HTTP transport...\n");
3998
4135
  for (const client of sseClients) {
3999
4136
  try {
4000
4137
  client.end();
@@ -4002,18 +4139,10 @@ protect-mcp HTTP server listening on port ${port}
4002
4139
  }
4003
4140
  }
4004
4141
  server.close();
4005
- process.exit(0);
4006
- });
4007
- }
4008
- async function processJsonRpc(gateway, request, config, serverCommand) {
4009
- return {
4010
- jsonrpc: "2.0",
4011
- result: {
4012
- message: "Request processed by protect-mcp HTTP transport",
4013
- mode: config.enforce ? "enforce" : "shadow"
4014
- },
4015
- id: request.id || null
4142
+ gateway.stop();
4016
4143
  };
4144
+ process.on("SIGINT", shutdown);
4145
+ process.on("SIGTERM", shutdown);
4017
4146
  }
4018
4147
  var import_node_http2;
4019
4148
  var init_http_transport = __esm({
package/dist/cli.mjs CHANGED
@@ -3,13 +3,13 @@ import {
3
3
  formatSimulation,
4
4
  parseLogFile,
5
5
  simulate
6
- } from "./chunk-UW2SGWCJ.mjs";
6
+ } from "./chunk-VWUN6AI6.mjs";
7
7
  import {
8
8
  ProtectGateway,
9
9
  initSigning,
10
10
  loadPolicy,
11
11
  validateCredentials
12
- } from "./chunk-XMZWJOC3.mjs";
12
+ } from "./chunk-7HBHIKLN.mjs";
13
13
 
14
14
  // src/cli.ts
15
15
  function printHelp() {
@@ -1028,7 +1028,7 @@ async function main() {
1028
1028
  if (useHttp) {
1029
1029
  const portIdx = args.indexOf("--port");
1030
1030
  const httpPort = portIdx >= 0 && args[portIdx + 1] ? parseInt(args[portIdx + 1]) : 3e3;
1031
- const { startHttpTransport } = await import("./http-transport-S7YXXMD3.mjs");
1031
+ const { startHttpTransport } = await import("./http-transport-RIVV2RVQ.mjs");
1032
1032
  startHttpTransport({ port: httpPort, config, serverCommand: childCommand });
1033
1033
  return;
1034
1034
  }
@@ -1,18 +1,25 @@
1
1
  import {
2
2
  ProtectGateway
3
- } from "./chunk-XMZWJOC3.mjs";
3
+ } from "./chunk-7HBHIKLN.mjs";
4
4
 
5
5
  // src/http-transport.ts
6
6
  import { createServer } from "http";
7
- function startHttpTransport(options) {
7
+ async function startHttpTransport(options) {
8
8
  const { port, config, serverCommand } = options;
9
9
  const sseClients = /* @__PURE__ */ new Set();
10
- const gateway = new ProtectGateway(config);
10
+ const httpConfig = {
11
+ ...config,
12
+ command: serverCommand[0],
13
+ args: serverCommand.slice(1)
14
+ };
15
+ const gateway = new ProtectGateway(httpConfig);
16
+ await gateway.startForHttp();
11
17
  const server = createServer(async (req, res) => {
12
18
  const origin = req.headers.origin || "*";
13
19
  res.setHeader("Access-Control-Allow-Origin", origin);
14
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
15
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
20
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
21
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id");
22
+ res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
16
23
  res.setHeader("Access-Control-Allow-Credentials", "true");
17
24
  if (req.method === "OPTIONS") {
18
25
  res.writeHead(204);
@@ -25,9 +32,10 @@ function startHttpTransport(options) {
25
32
  res.end(JSON.stringify({
26
33
  status: "ok",
27
34
  server: "protect-mcp",
28
- version: "0.3.3",
29
- transport: "http",
30
- mode: config.policy ? config.enforce ? "enforce" : "shadow" : "shadow"
35
+ version: "0.4.0",
36
+ transport: "streamable-http",
37
+ mode: config.policy ? config.enforce ? "enforce" : "shadow" : "shadow",
38
+ wrapping: serverCommand.join(" ")
31
39
  }));
32
40
  return;
33
41
  }
@@ -53,28 +61,35 @@ function startHttpTransport(options) {
53
61
  try {
54
62
  const jsonRpc = JSON.parse(body);
55
63
  const acceptSSE = (req.headers.accept || "").includes("text/event-stream");
64
+ const responseStr = await gateway.processRequest(jsonRpc);
65
+ const response = JSON.parse(responseStr);
56
66
  if (acceptSSE) {
57
67
  res.writeHead(200, {
58
68
  "Content-Type": "text/event-stream",
59
69
  "Cache-Control": "no-cache"
60
70
  });
61
- const response = await processJsonRpc(gateway, jsonRpc, config, serverCommand);
62
71
  res.write(`data: ${JSON.stringify(response)}
63
72
 
64
73
  `);
65
74
  res.end();
66
75
  } else {
67
- const response = await processJsonRpc(gateway, jsonRpc, config, serverCommand);
68
76
  res.writeHead(200, { "Content-Type": "application/json" });
69
77
  res.end(JSON.stringify(response));
70
78
  }
71
- for (const client of sseClients) {
72
- try {
73
- client.write(`data: ${JSON.stringify({ type: "decision", request: jsonRpc, timestamp: (/* @__PURE__ */ new Date()).toISOString() })}
79
+ if (jsonRpc.method === "tools/call") {
80
+ const event = {
81
+ type: "decision",
82
+ tool: jsonRpc.params?.name,
83
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
84
+ };
85
+ for (const client of sseClients) {
86
+ try {
87
+ client.write(`data: ${JSON.stringify(event)}
74
88
 
75
89
  `);
76
- } catch {
77
- sseClients.delete(client);
90
+ } catch {
91
+ sseClients.delete(client);
92
+ }
78
93
  }
79
94
  }
80
95
  } catch (err) {
@@ -88,24 +103,43 @@ function startHttpTransport(options) {
88
103
  });
89
104
  return;
90
105
  }
106
+ if (url.pathname === "/mcp" && req.method === "DELETE") {
107
+ res.writeHead(200, { "Content-Type": "application/json" });
108
+ res.end(JSON.stringify({ status: "session_closed" }));
109
+ return;
110
+ }
91
111
  res.writeHead(404, { "Content-Type": "application/json" });
92
- res.end(JSON.stringify({ error: "not_found", endpoints: ["/mcp", "/mcp/sse", "/health"] }));
112
+ res.end(JSON.stringify({
113
+ error: "not_found",
114
+ endpoints: [
115
+ "POST /mcp \u2014 JSON-RPC endpoint (Streamable HTTP)",
116
+ "GET /mcp/sse \u2014 Server-Sent Events stream",
117
+ "GET /health \u2014 Health check",
118
+ "DELETE /mcp \u2014 Close session"
119
+ ]
120
+ }));
93
121
  });
94
122
  server.listen(port, () => {
95
123
  process.stderr.write(`
96
- protect-mcp HTTP server listening on port ${port}
124
+ [PROTECT_MCP] HTTP transport listening on http://0.0.0.0:${port}
125
+ `);
126
+ process.stderr.write(` POST /mcp \u2014 JSON-RPC (Streamable HTTP)
97
127
  `);
98
- process.stderr.write(` POST /mcp \u2014 JSON-RPC endpoint (Streamable HTTP)
128
+ process.stderr.write(` GET /mcp/sse \u2014 Server-Sent Events
99
129
  `);
100
- process.stderr.write(` GET /mcp/sse \u2014 Server-Sent Events stream
130
+ process.stderr.write(` GET /health \u2014 Health check
101
131
  `);
102
- process.stderr.write(` GET /health \u2014 Health check
132
+ process.stderr.write(` DELETE /mcp \u2014 Close session
133
+ `);
134
+ process.stderr.write(`
135
+ Wrapping: ${serverCommand.join(" ")}
136
+ `);
137
+ process.stderr.write(` Mode: ${config.enforce ? "enforce" : "shadow"}
103
138
 
104
139
  `);
105
140
  });
106
- gateway.start(serverCommand[0], serverCommand.slice(1));
107
- process.on("SIGINT", () => {
108
- process.stderr.write("\nShutting down HTTP transport...\n");
141
+ const shutdown = () => {
142
+ process.stderr.write("\n[PROTECT_MCP] Shutting down HTTP transport...\n");
109
143
  for (const client of sseClients) {
110
144
  try {
111
145
  client.end();
@@ -113,18 +147,10 @@ protect-mcp HTTP server listening on port ${port}
113
147
  }
114
148
  }
115
149
  server.close();
116
- process.exit(0);
117
- });
118
- }
119
- async function processJsonRpc(gateway, request, config, serverCommand) {
120
- return {
121
- jsonrpc: "2.0",
122
- result: {
123
- message: "Request processed by protect-mcp HTTP transport",
124
- mode: config.enforce ? "enforce" : "shadow"
125
- },
126
- id: request.id || null
150
+ gateway.stop();
127
151
  };
152
+ process.on("SIGINT", shutdown);
153
+ process.on("SIGTERM", shutdown);
128
154
  }
129
155
  export {
130
156
  startHttpTransport
package/dist/index.d.mts CHANGED
@@ -316,6 +316,9 @@ declare class ProtectGateway {
316
316
  private readonly approvalNonce;
317
317
  private currentTier;
318
318
  private admissionResult;
319
+ /** HTTP transport mode: pending response resolvers keyed by JSON-RPC id */
320
+ private pendingResponses;
321
+ private httpMode;
319
322
  constructor(config: ProtectConfig);
320
323
  start(): Promise<void>;
321
324
  setManifest(manifest: ManifestPresentation | null): AdmissionResult;
@@ -329,6 +332,22 @@ declare class ProtectGateway {
329
332
  private makeErrorResponse;
330
333
  private sendToChild;
331
334
  private sendToClient;
335
+ /**
336
+ * Enable HTTP transport mode.
337
+ * In this mode, sendToClient resolves pending promises instead of
338
+ * writing to stdout, and start() skips stdin reading.
339
+ */
340
+ enableHttpMode(): void;
341
+ /**
342
+ * Start in HTTP mode — spawns child process but does NOT read from
343
+ * process.stdin. Requests come in via processRequest() instead.
344
+ */
345
+ startForHttp(): Promise<void>;
346
+ /**
347
+ * Process a JSON-RPC request programmatically (for HTTP transport).
348
+ * Returns a promise that resolves with the JSON-RPC response string.
349
+ */
350
+ processRequest(jsonRpc: JsonRpcRequest): Promise<string>;
332
351
  private log;
333
352
  stop(): void;
334
353
  }
package/dist/index.d.ts CHANGED
@@ -316,6 +316,9 @@ declare class ProtectGateway {
316
316
  private readonly approvalNonce;
317
317
  private currentTier;
318
318
  private admissionResult;
319
+ /** HTTP transport mode: pending response resolvers keyed by JSON-RPC id */
320
+ private pendingResponses;
321
+ private httpMode;
319
322
  constructor(config: ProtectConfig);
320
323
  start(): Promise<void>;
321
324
  setManifest(manifest: ManifestPresentation | null): AdmissionResult;
@@ -329,6 +332,22 @@ declare class ProtectGateway {
329
332
  private makeErrorResponse;
330
333
  private sendToChild;
331
334
  private sendToClient;
335
+ /**
336
+ * Enable HTTP transport mode.
337
+ * In this mode, sendToClient resolves pending promises instead of
338
+ * writing to stdout, and start() skips stdin reading.
339
+ */
340
+ enableHttpMode(): void;
341
+ /**
342
+ * Start in HTTP mode — spawns child process but does NOT read from
343
+ * process.stdin. Requests come in via processRequest() instead.
344
+ */
345
+ startForHttp(): Promise<void>;
346
+ /**
347
+ * Process a JSON-RPC request programmatically (for HTTP transport).
348
+ * Returns a promise that resolves with the JSON-RPC response string.
349
+ */
350
+ processRequest(jsonRpc: JsonRpcRequest): Promise<string>;
332
351
  private log;
333
352
  stop(): void;
334
353
  }
package/dist/index.js CHANGED
@@ -868,6 +868,9 @@ var ProtectGateway = class {
868
868
  approvalNonce = (0, import_node_crypto2.randomBytes)(16).toString("hex");
869
869
  currentTier = "unknown";
870
870
  admissionResult = null;
871
+ /** HTTP transport mode: pending response resolvers keyed by JSON-RPC id */
872
+ pendingResponses = /* @__PURE__ */ new Map();
873
+ httpMode = false;
871
874
  constructor(config) {
872
875
  this.config = config;
873
876
  this.logFilePath = (0, import_node_path3.join)(process.cwd(), LOG_FILE2);
@@ -1187,8 +1190,108 @@ var ProtectGateway = class {
1187
1190
  if (this.child?.stdin?.writable) this.child.stdin.write(message + "\n");
1188
1191
  }
1189
1192
  sendToClient(message) {
1193
+ if (this.httpMode) {
1194
+ try {
1195
+ const parsed = JSON.parse(message);
1196
+ if (parsed.id !== void 0 && parsed.id !== null) {
1197
+ const pending = this.pendingResponses.get(parsed.id);
1198
+ if (pending) {
1199
+ clearTimeout(pending.timeout);
1200
+ this.pendingResponses.delete(parsed.id);
1201
+ pending.resolve(message);
1202
+ return;
1203
+ }
1204
+ }
1205
+ } catch {
1206
+ }
1207
+ }
1190
1208
  process.stdout.write(message + "\n");
1191
1209
  }
1210
+ /**
1211
+ * Enable HTTP transport mode.
1212
+ * In this mode, sendToClient resolves pending promises instead of
1213
+ * writing to stdout, and start() skips stdin reading.
1214
+ */
1215
+ enableHttpMode() {
1216
+ this.httpMode = true;
1217
+ }
1218
+ /**
1219
+ * Start in HTTP mode — spawns child process but does NOT read from
1220
+ * process.stdin. Requests come in via processRequest() instead.
1221
+ */
1222
+ async startForHttp() {
1223
+ this.httpMode = true;
1224
+ const { command, args, verbose } = this.config;
1225
+ const mode = this.config.enforce ? "enforce" : "shadow";
1226
+ if (verbose) {
1227
+ this.log(`Starting gateway in ${mode} mode (HTTP transport)`);
1228
+ this.log(`Wrapping: ${command} ${args.join(" ")}`);
1229
+ }
1230
+ this.log(`Approval nonce: ${this.approvalNonce}`);
1231
+ const childEnv = { ...process.env };
1232
+ if (this.config.credentials) {
1233
+ for (const [label, credConfig] of Object.entries(this.config.credentials)) {
1234
+ if (credConfig.inject === "env" && credConfig.name && credConfig.value_env) {
1235
+ const envValue = process.env[credConfig.value_env];
1236
+ if (envValue) {
1237
+ childEnv[credConfig.name] = envValue;
1238
+ if (verbose) this.log(`Credential "${label}": injected as env var "${credConfig.name}"`);
1239
+ }
1240
+ }
1241
+ }
1242
+ }
1243
+ this.child = (0, import_node_child_process.spawn)(command, args, { stdio: ["pipe", "pipe", "pipe"], env: childEnv });
1244
+ if (!this.child.stdin || !this.child.stdout || !this.child.stderr) {
1245
+ throw new Error("Failed to create pipes to child process");
1246
+ }
1247
+ this.child.stderr.on("data", (data) => {
1248
+ process.stderr.write(data);
1249
+ });
1250
+ const childReader = (0, import_node_readline.createInterface)({ input: this.child.stdout, crlfDelay: Infinity });
1251
+ childReader.on("line", (line) => {
1252
+ this.handleServerMessage(line);
1253
+ });
1254
+ this.child.on("exit", (code, signal) => {
1255
+ if (verbose) this.log(`Child process exited (code=${code}, signal=${signal})`);
1256
+ this.evidenceStore.save();
1257
+ });
1258
+ this.child.on("error", (err) => {
1259
+ this.log(`Child process error: ${err.message}`);
1260
+ });
1261
+ }
1262
+ /**
1263
+ * Process a JSON-RPC request programmatically (for HTTP transport).
1264
+ * Returns a promise that resolves with the JSON-RPC response string.
1265
+ */
1266
+ async processRequest(jsonRpc) {
1267
+ const REQUEST_TIMEOUT_MS = 3e4;
1268
+ if (jsonRpc.method === "tools/call" && jsonRpc.id !== void 0) {
1269
+ const blocked = await this.interceptToolCall(jsonRpc);
1270
+ if (blocked) {
1271
+ return JSON.stringify(blocked);
1272
+ }
1273
+ }
1274
+ return new Promise((resolve, reject) => {
1275
+ const id = jsonRpc.id;
1276
+ if (id === void 0 || id === null) {
1277
+ const modified2 = this.injectParamsCredentials(jsonRpc);
1278
+ this.sendToChild(JSON.stringify(modified2));
1279
+ resolve(JSON.stringify({ jsonrpc: "2.0", result: {}, id: null }));
1280
+ return;
1281
+ }
1282
+ const timeout = setTimeout(() => {
1283
+ this.pendingResponses.delete(id);
1284
+ resolve(JSON.stringify({
1285
+ jsonrpc: "2.0",
1286
+ error: { code: -32e3, message: "Request timeout (30s)" },
1287
+ id
1288
+ }));
1289
+ }, REQUEST_TIMEOUT_MS);
1290
+ this.pendingResponses.set(id, { resolve, timeout });
1291
+ const modified = this.injectParamsCredentials(jsonRpc);
1292
+ this.sendToChild(JSON.stringify(modified));
1293
+ });
1294
+ }
1192
1295
  log(message) {
1193
1296
  process.stderr.write(`[PROTECT_MCP] ${message}
1194
1297
  `);
package/dist/index.mjs CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  formatSimulation,
3
3
  parseLogFile,
4
4
  simulate
5
- } from "./chunk-UW2SGWCJ.mjs";
5
+ } from "./chunk-VWUN6AI6.mjs";
6
6
  import {
7
7
  collectSignedReceipts,
8
8
  createAuditBundle
@@ -24,7 +24,7 @@ import {
24
24
  resolveCredential,
25
25
  signDecision,
26
26
  validateCredentials
27
- } from "./chunk-XMZWJOC3.mjs";
27
+ } from "./chunk-7HBHIKLN.mjs";
28
28
  import {
29
29
  formatReportMarkdown,
30
30
  generateReport
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "protect-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "mcpName": "io.github.tomjwxf/protect-mcp",
5
5
  "description": "Security gateway for MCP servers. Shadow-mode logs, per-tool policies, optional local Ed25519-signed receipts. Programmatic hooks for trust tiers, credential config, and external policy engines.",
6
6
  "main": "dist/index.js",