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.
- package/dist/{chunk-XMZWJOC3.mjs → chunk-7HBHIKLN.mjs} +103 -0
- package/dist/{chunk-UW2SGWCJ.mjs → chunk-VWUN6AI6.mjs} +1 -1
- package/dist/cli.js +162 -33
- package/dist/cli.mjs +3 -3
- package/dist/{http-transport-S7YXXMD3.mjs → http-transport-RIVV2RVQ.mjs} +60 -34
- package/dist/index.d.mts +19 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +103 -0
- package/dist/index.mjs +2 -2
- package/package.json +1 -1
|
@@ -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
|
`);
|
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
|
|
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.
|
|
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
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
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
|
-
|
|
3966
|
-
|
|
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({
|
|
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
|
-
|
|
4116
|
+
[PROTECT_MCP] HTTP transport listening on http://0.0.0.0:${port}
|
|
3986
4117
|
`);
|
|
3987
|
-
process.stderr.write(` POST
|
|
4118
|
+
process.stderr.write(` POST /mcp \u2014 JSON-RPC (Streamable HTTP)
|
|
3988
4119
|
`);
|
|
3989
|
-
process.stderr.write(` GET
|
|
4120
|
+
process.stderr.write(` GET /mcp/sse \u2014 Server-Sent Events
|
|
3990
4121
|
`);
|
|
3991
|
-
process.stderr.write(` GET
|
|
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
|
-
|
|
3996
|
-
|
|
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
|
-
|
|
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-
|
|
6
|
+
} from "./chunk-VWUN6AI6.mjs";
|
|
7
7
|
import {
|
|
8
8
|
ProtectGateway,
|
|
9
9
|
initSigning,
|
|
10
10
|
loadPolicy,
|
|
11
11
|
validateCredentials
|
|
12
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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
|
|
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.
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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({
|
|
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
|
-
|
|
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(`
|
|
128
|
+
process.stderr.write(` GET /mcp/sse \u2014 Server-Sent Events
|
|
99
129
|
`);
|
|
100
|
-
process.stderr.write(` GET
|
|
130
|
+
process.stderr.write(` GET /health \u2014 Health check
|
|
101
131
|
`);
|
|
102
|
-
process.stderr.write(`
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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.
|
|
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",
|