@vengtoo/mcp-gateway 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 +200 -0
- package/README.md +227 -0
- package/dist/audit.d.ts +29 -0
- package/dist/audit.js +146 -0
- package/dist/authorize.d.ts +2 -0
- package/dist/authorize.js +96 -0
- package/dist/classify.d.ts +8 -0
- package/dist/classify.js +24 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +104 -0
- package/dist/demo.d.ts +1 -0
- package/dist/demo.js +164 -0
- package/dist/drift.d.ts +4 -0
- package/dist/drift.js +128 -0
- package/dist/gateway-id.d.ts +1 -0
- package/dist/gateway-id.js +18 -0
- package/dist/gateway.d.ts +2 -0
- package/dist/gateway.js +173 -0
- package/dist/generate-policy.d.ts +2 -0
- package/dist/generate-policy.js +123 -0
- package/dist/hash.d.ts +7 -0
- package/dist/hash.js +39 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/list-tools.d.ts +2 -0
- package/dist/list-tools.js +51 -0
- package/dist/snapshot.d.ts +3 -0
- package/dist/snapshot.js +34 -0
- package/dist/sync.d.ts +11 -0
- package/dist/sync.js +140 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.js +2 -0
- package/dist/utils.d.ts +14 -0
- package/dist/utils.js +33 -0
- package/package.json +38 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.authorize = void 0;
|
|
4
|
+
const DEFAULT_CLOUD_URL = "https://api.vengtoo.com/access/v1/evaluation";
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
6
|
+
function normalizeResponse(raw) {
|
|
7
|
+
const allowed = raw.decision ?? raw.allowed ?? false;
|
|
8
|
+
const reason = raw.context?.reason ?? raw.reason;
|
|
9
|
+
return { allowed, reason };
|
|
10
|
+
}
|
|
11
|
+
async function authorize(config, subjectId, toolName, args) {
|
|
12
|
+
const body = {
|
|
13
|
+
subject: { type: config.subjectType ?? "agent", id: subjectId },
|
|
14
|
+
resource: { type: config.resourceType ?? "mcp_tool", name: toolName, attributes: args },
|
|
15
|
+
action: { name: "invoke" },
|
|
16
|
+
};
|
|
17
|
+
const timeoutMs = config.vengtoo.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
18
|
+
const auth = authHeader(config);
|
|
19
|
+
const endpoints = resolveEndpoints(config);
|
|
20
|
+
let lastError;
|
|
21
|
+
for (let i = 0; i < endpoints.length; i++) {
|
|
22
|
+
const url = endpoints[i];
|
|
23
|
+
const isLast = i === endpoints.length - 1;
|
|
24
|
+
try {
|
|
25
|
+
const res = await postJson(url, body, auth, timeoutMs);
|
|
26
|
+
if (res.ok)
|
|
27
|
+
return normalizeResponse((await res.json()));
|
|
28
|
+
const text = await safeText(res);
|
|
29
|
+
if (res.status === 401) {
|
|
30
|
+
return deny(`authentication failed — check your API key`);
|
|
31
|
+
}
|
|
32
|
+
if (res.status === 404) {
|
|
33
|
+
return deny(`no authorization policy configured for this tool`);
|
|
34
|
+
}
|
|
35
|
+
if (res.status >= 500 && !isLast) {
|
|
36
|
+
lastError = new Error(`${url}: ${res.status} ${text}`);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
return deny(`authorization service error (${res.status})`);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
lastError = err;
|
|
43
|
+
if (isLast)
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
console.error("[vengtoo-gateway] authorize failed:", lastError);
|
|
48
|
+
return deny(`authz unreachable: ${lastError?.message ?? "unknown"}`);
|
|
49
|
+
}
|
|
50
|
+
exports.authorize = authorize;
|
|
51
|
+
function resolveEndpoints(config) {
|
|
52
|
+
const urls = [];
|
|
53
|
+
const a = config.vengtoo;
|
|
54
|
+
if (a.agentUrl) {
|
|
55
|
+
const base = a.agentUrl.replace(/\/$/, "");
|
|
56
|
+
urls.push(base.endsWith("/access/v1/evaluation") ? base : `${base}/access/v1/evaluation`);
|
|
57
|
+
}
|
|
58
|
+
if (a.cloudUrl)
|
|
59
|
+
urls.push(a.cloudUrl);
|
|
60
|
+
if (urls.length === 0)
|
|
61
|
+
urls.push(DEFAULT_CLOUD_URL);
|
|
62
|
+
return urls;
|
|
63
|
+
}
|
|
64
|
+
function authHeader(config) {
|
|
65
|
+
const a = config.vengtoo;
|
|
66
|
+
if (a.apiKey)
|
|
67
|
+
return `Bearer ${a.apiKey}`;
|
|
68
|
+
if (a.clientId && a.clientSecret) {
|
|
69
|
+
return `Basic ${Buffer.from(`${a.clientId}:${a.clientSecret}`).toString("base64")}`;
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
async function postJson(url, body, auth, timeoutMs) {
|
|
74
|
+
const headers = { "Content-Type": "application/json", Accept: "application/json" };
|
|
75
|
+
if (auth)
|
|
76
|
+
headers["Authorization"] = auth;
|
|
77
|
+
const controller = new AbortController();
|
|
78
|
+
const t = setTimeout(() => controller.abort(), timeoutMs);
|
|
79
|
+
try {
|
|
80
|
+
return await fetch(url, { method: "POST", headers, body: JSON.stringify(body), signal: controller.signal });
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
clearTimeout(t);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function safeText(res) {
|
|
87
|
+
try {
|
|
88
|
+
return await res.text();
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return "";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function deny(reason) {
|
|
95
|
+
return { allowed: false, reason };
|
|
96
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DiscoveredTool } from "./utils";
|
|
2
|
+
export type TrustLevel = "low" | "medium" | "high";
|
|
3
|
+
export interface ClassifiedTool extends DiscoveredTool {
|
|
4
|
+
trust: TrustLevel;
|
|
5
|
+
schema: unknown;
|
|
6
|
+
}
|
|
7
|
+
export declare function classifyTool(toolName: string): TrustLevel;
|
|
8
|
+
export declare function classifyTools(tools: DiscoveredTool[], schemas: Map<string, unknown>): ClassifiedTool[];
|
package/dist/classify.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.classifyTools = exports.classifyTool = void 0;
|
|
4
|
+
const HIGH = /delete|remove|destroy|drop|purge|wipe/i;
|
|
5
|
+
const MEDIUM = /create|write|update|insert|modify|send|post|execute|mutation/i;
|
|
6
|
+
function classifyTool(toolName) {
|
|
7
|
+
const action = toolName.includes("__")
|
|
8
|
+
? toolName.split("__").pop()
|
|
9
|
+
: toolName;
|
|
10
|
+
if (HIGH.test(action))
|
|
11
|
+
return "high";
|
|
12
|
+
if (MEDIUM.test(action))
|
|
13
|
+
return "medium";
|
|
14
|
+
return "low";
|
|
15
|
+
}
|
|
16
|
+
exports.classifyTool = classifyTool;
|
|
17
|
+
function classifyTools(tools, schemas) {
|
|
18
|
+
return tools.map((t) => ({
|
|
19
|
+
...t,
|
|
20
|
+
trust: classifyTool(t.qualifiedName),
|
|
21
|
+
schema: schemas.get(t.qualifiedName) ?? {},
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
exports.classifyTools = classifyTools;
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
20
|
+
if (mod && mod.__esModule) return mod;
|
|
21
|
+
var result = {};
|
|
22
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
23
|
+
__setModuleDefault(result, mod);
|
|
24
|
+
return result;
|
|
25
|
+
};
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
const fs_1 = require("fs");
|
|
28
|
+
const path_1 = require("path");
|
|
29
|
+
const gateway_1 = require("./gateway");
|
|
30
|
+
const config = loadConfig();
|
|
31
|
+
if (process.argv.includes("--list-tools")) {
|
|
32
|
+
Promise.resolve().then(() => __importStar(require("./list-tools"))).then((m) => m.listTools(config));
|
|
33
|
+
}
|
|
34
|
+
else if (process.argv.includes("--generate-policy")) {
|
|
35
|
+
const outFlag = process.argv.indexOf("--generate-policy");
|
|
36
|
+
const outPath = process.argv[outFlag + 1] || "policy.rego";
|
|
37
|
+
Promise.resolve().then(() => __importStar(require("./generate-policy"))).then((m) => m.generatePolicy(config, outPath));
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
(0, gateway_1.startGateway)(config).catch((err) => {
|
|
41
|
+
console.error("[vengtoo-gateway] fatal:", err);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function loadConfig() {
|
|
46
|
+
const configFlag = process.argv.indexOf("--config");
|
|
47
|
+
const configPath = configFlag !== -1 && process.argv[configFlag + 1]
|
|
48
|
+
? (0, path_1.resolve)(process.argv[configFlag + 1])
|
|
49
|
+
: (0, path_1.resolve)("gateway.config.json");
|
|
50
|
+
try {
|
|
51
|
+
const raw = (0, fs_1.readFileSync)(configPath, "utf-8");
|
|
52
|
+
const config = JSON.parse(raw);
|
|
53
|
+
if (!config.vengtoo)
|
|
54
|
+
throw new Error("missing 'vengtoo' section");
|
|
55
|
+
if (!config.subject)
|
|
56
|
+
throw new Error("missing 'subject'");
|
|
57
|
+
if (!config.servers || Object.keys(config.servers).length === 0) {
|
|
58
|
+
throw new Error("missing 'servers' — configure at least one downstream MCP server");
|
|
59
|
+
}
|
|
60
|
+
if (process.env.VENGTOO_API_KEY)
|
|
61
|
+
config.vengtoo.apiKey = process.env.VENGTOO_API_KEY;
|
|
62
|
+
if (process.env.VENGTOO_AGENT_URL)
|
|
63
|
+
config.vengtoo.agentUrl = process.env.VENGTOO_AGENT_URL;
|
|
64
|
+
if (process.env.VENGTOO_SUBJECT)
|
|
65
|
+
config.subject = process.env.VENGTOO_SUBJECT;
|
|
66
|
+
if (config.vengtoo.agentUrl) {
|
|
67
|
+
try {
|
|
68
|
+
new URL(config.vengtoo.agentUrl);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new Error(`'vengtoo.agentUrl' is not a valid URL: ${config.vengtoo.agentUrl}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (config.vengtoo.cloudUrl) {
|
|
75
|
+
try {
|
|
76
|
+
new URL(config.vengtoo.cloudUrl);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
throw new Error(`'vengtoo.cloudUrl' is not a valid URL: ${config.vengtoo.cloudUrl}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const [name, srv] of Object.entries(config.servers)) {
|
|
83
|
+
if (!srv.command || typeof srv.command !== "string") {
|
|
84
|
+
throw new Error(`server '${name}' must have a non-empty 'command' string`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (config.vengtoo.timeoutMs !== undefined) {
|
|
88
|
+
if (typeof config.vengtoo.timeoutMs !== "number" || config.vengtoo.timeoutMs <= 0) {
|
|
89
|
+
throw new Error(`'vengtoo.timeoutMs' must be a positive number`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return config;
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
96
|
+
console.error(`Config not found: ${configPath}`);
|
|
97
|
+
console.error(`Usage: vengtoo-mcp-gateway --config <path>`);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
101
|
+
console.error(`Invalid config: ${message}`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
}
|
package/dist/demo.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runDemo(): Promise<void>;
|
package/dist/demo.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runDemo = void 0;
|
|
4
|
+
const path_1 = require("path");
|
|
5
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
|
|
6
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
7
|
+
const AGENT_PORT = 8181;
|
|
8
|
+
const AGENT_URL = `http://localhost:${AGENT_PORT}`;
|
|
9
|
+
const TESTS = [
|
|
10
|
+
{ name: "SELECT query", tool: "database__query", args: { sql: "SELECT * FROM users" }, expectAllow: true },
|
|
11
|
+
{ name: "List tables", tool: "database__list_tables", args: {}, expectAllow: true },
|
|
12
|
+
{ name: "Describe table", tool: "database__describe_table", args: { table: "users" }, expectAllow: true },
|
|
13
|
+
{ name: "INSERT (safe write)", tool: "database__execute", args: { sql: "INSERT INTO users (email) VALUES ('test@test.com')" }, expectAllow: true },
|
|
14
|
+
{ name: "DROP TABLE (destructive)", tool: "database__execute", args: { sql: "DROP TABLE users" }, expectAllow: false },
|
|
15
|
+
{ name: "TRUNCATE (destructive)", tool: "database__execute", args: { sql: "TRUNCATE TABLE users" }, expectAllow: false },
|
|
16
|
+
{ name: "DELETE FROM (destructive)", tool: "database__execute", args: { sql: "DELETE FROM users WHERE id = 1" }, expectAllow: false },
|
|
17
|
+
{ name: "ALTER TABLE (destructive)", tool: "database__execute", args: { sql: "ALTER TABLE users DROP COLUMN email" }, expectAllow: false },
|
|
18
|
+
];
|
|
19
|
+
function log(msg) { console.error(msg); }
|
|
20
|
+
function pass(msg) { console.error(` \x1b[32m✓\x1b[0m ${msg}`); }
|
|
21
|
+
function fail(msg) { console.error(` \x1b[31m✗\x1b[0m ${msg}`); }
|
|
22
|
+
function bold(msg) { return `\x1b[1m${msg}\x1b[0m`; }
|
|
23
|
+
async function waitForAgent(timeoutMs = 10_000) {
|
|
24
|
+
const start = Date.now();
|
|
25
|
+
while (Date.now() - start < timeoutMs) {
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch(`${AGENT_URL}/readyz`);
|
|
28
|
+
if (res.ok)
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
catch { }
|
|
32
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
function findDemoDir() {
|
|
37
|
+
// Works from dist/ (compiled) or src/ (dev)
|
|
38
|
+
const thisDir = __dirname;
|
|
39
|
+
const candidates = [
|
|
40
|
+
(0, path_1.resolve)(thisDir, "..", "demo"), // dist/../demo
|
|
41
|
+
(0, path_1.resolve)(thisDir, "..", "..", "demo"), // nested
|
|
42
|
+
];
|
|
43
|
+
for (const c of candidates) {
|
|
44
|
+
try {
|
|
45
|
+
require("fs").accessSync((0, path_1.resolve)(c, "mock-servers", "database-server.mjs"));
|
|
46
|
+
return c;
|
|
47
|
+
}
|
|
48
|
+
catch { }
|
|
49
|
+
}
|
|
50
|
+
throw new Error("Could not find demo directory. Run from the authzx-mcp-gateway package root.");
|
|
51
|
+
}
|
|
52
|
+
async function runDemo() {
|
|
53
|
+
log("");
|
|
54
|
+
log(bold(" AuthzX MCP Gateway — Live Demo"));
|
|
55
|
+
log(" ─────────────────────────────────────────");
|
|
56
|
+
log("");
|
|
57
|
+
log(" This demo shows the gateway blocking destructive database");
|
|
58
|
+
log(" operations while allowing reads and safe writes.");
|
|
59
|
+
log("");
|
|
60
|
+
const demoDir = findDemoDir();
|
|
61
|
+
const policyPath = (0, path_1.resolve)(demoDir, "policies", "mcp-database-policy.rego");
|
|
62
|
+
const mockServerPath = (0, path_1.resolve)(demoDir, "mock-servers", "database-server.mjs");
|
|
63
|
+
const cliPath = (0, path_1.resolve)(__dirname, "cli.js");
|
|
64
|
+
// Step 1: Check if agent is running, or try to start it
|
|
65
|
+
log(bold(" Step 1: AuthzX Agent"));
|
|
66
|
+
log("");
|
|
67
|
+
log(" The agent evaluates your Rego policy on every tool call.");
|
|
68
|
+
log(" It must be running before the gateway can work.");
|
|
69
|
+
log("");
|
|
70
|
+
const agentReady = await waitForAgent(2_000);
|
|
71
|
+
if (agentReady) {
|
|
72
|
+
log(" \x1b[32m✓\x1b[0m Agent detected on :8181");
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
log(" \x1b[31m✗ Agent not running on :8181\x1b[0m");
|
|
76
|
+
log("");
|
|
77
|
+
log(" Start it in another terminal:");
|
|
78
|
+
log("");
|
|
79
|
+
log(" # 1. Install the agent (Go binary)");
|
|
80
|
+
log(" # Download from: https://github.com/authzx/authzx-agent/releases");
|
|
81
|
+
log(" # Or build from source: go build -o authzx-agent ./cmd/agent/");
|
|
82
|
+
log("");
|
|
83
|
+
log(" # 2. Run with the demo policy");
|
|
84
|
+
log(` authzx-agent --policy ${policyPath}`);
|
|
85
|
+
log("");
|
|
86
|
+
log(" Then run this demo again.");
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
// Step 2: Start gateway with mock database server
|
|
90
|
+
log("");
|
|
91
|
+
log(bold(" Step 2: Gateway + Mock Database Server"));
|
|
92
|
+
const gatewayConfig = {
|
|
93
|
+
authzx: { agentUrl: AGENT_URL },
|
|
94
|
+
subject: "agent:demo",
|
|
95
|
+
servers: {
|
|
96
|
+
database: {
|
|
97
|
+
command: "node",
|
|
98
|
+
args: [mockServerPath],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
const configPath = (0, path_1.resolve)(demoDir, ".demo-runtime-config.json");
|
|
103
|
+
require("fs").writeFileSync(configPath, JSON.stringify(gatewayConfig, null, 2));
|
|
104
|
+
const transport = new stdio_js_1.StdioClientTransport({
|
|
105
|
+
command: "node",
|
|
106
|
+
args: [cliPath, "--config", configPath],
|
|
107
|
+
});
|
|
108
|
+
const client = new index_js_1.Client({ name: "authzx-demo", version: "1.0.0" }, { capabilities: {} });
|
|
109
|
+
await client.connect(transport);
|
|
110
|
+
const { tools } = await client.listTools();
|
|
111
|
+
log(` Gateway started — ${tools.length} tools registered`);
|
|
112
|
+
for (const t of tools) {
|
|
113
|
+
log(` • ${t.name}`);
|
|
114
|
+
}
|
|
115
|
+
// Step 3: Run tests
|
|
116
|
+
log("");
|
|
117
|
+
log(bold(" Step 3: Authorization Tests"));
|
|
118
|
+
log("");
|
|
119
|
+
let passed = 0;
|
|
120
|
+
let failed_count = 0;
|
|
121
|
+
for (const test of TESTS) {
|
|
122
|
+
const result = await client.callTool({ name: test.tool, arguments: test.args });
|
|
123
|
+
const isError = result.isError === true;
|
|
124
|
+
const text = result.content[0]?.text ?? "";
|
|
125
|
+
const wasDenied = isError && text.includes("Authorization denied");
|
|
126
|
+
const wasAllowed = !wasDenied;
|
|
127
|
+
if (wasAllowed === test.expectAllow) {
|
|
128
|
+
const label = test.expectAllow ? "ALLOWED" : "DENIED";
|
|
129
|
+
pass(`${test.name} → ${label}`);
|
|
130
|
+
passed++;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const expected = test.expectAllow ? "ALLOW" : "DENY";
|
|
134
|
+
const got = wasAllowed ? "ALLOW" : "DENY";
|
|
135
|
+
fail(`${test.name} → expected ${expected}, got ${got}`);
|
|
136
|
+
failed_count++;
|
|
137
|
+
}
|
|
138
|
+
if (wasDenied) {
|
|
139
|
+
const reason = text.replace("Authorization denied for tool \"" + test.tool + "\": ", "");
|
|
140
|
+
log(` └─ ${reason}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Cleanup
|
|
144
|
+
try {
|
|
145
|
+
require("fs").unlinkSync(configPath);
|
|
146
|
+
}
|
|
147
|
+
catch { }
|
|
148
|
+
log("");
|
|
149
|
+
log(" ─────────────────────────────────────────");
|
|
150
|
+
log(` ${bold(`${passed}/${TESTS.length} passed`)}${failed_count > 0 ? ` (${failed_count} failed)` : ""}`);
|
|
151
|
+
log("");
|
|
152
|
+
if (failed_count === 0) {
|
|
153
|
+
log(" \x1b[32m✓ All tests passed!\x1b[0m The gateway correctly blocked");
|
|
154
|
+
log(" destructive operations while allowing safe ones.");
|
|
155
|
+
log("");
|
|
156
|
+
log(" Next steps:");
|
|
157
|
+
log(" 1. Replace the mock server with your real MCP servers");
|
|
158
|
+
log(" 2. Edit the Rego policy to match your requirements");
|
|
159
|
+
log(" 3. Add the gateway to your .mcp.json");
|
|
160
|
+
log("");
|
|
161
|
+
}
|
|
162
|
+
process.exit(failed_count > 0 ? 1 : 0);
|
|
163
|
+
}
|
|
164
|
+
exports.runDemo = runDemo;
|
package/dist/drift.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DriftEvent, DriftSeverity, ServerSnapshot, ToolSnapshot } from "./types";
|
|
2
|
+
export declare function detectDrift(serverName: string, oldSnapshot: ServerSnapshot, newTools: ToolSnapshot[]): DriftEvent[];
|
|
3
|
+
export declare function maxSeverity(events: DriftEvent[]): DriftSeverity | null;
|
|
4
|
+
export declare function logDriftEvents(events: DriftEvent[]): void;
|
package/dist/drift.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.logDriftEvents = exports.maxSeverity = exports.detectDrift = void 0;
|
|
4
|
+
function detectDrift(serverName, oldSnapshot, newTools) {
|
|
5
|
+
const events = [];
|
|
6
|
+
const oldMap = new Map(oldSnapshot.tools.map((t) => [t.name, t]));
|
|
7
|
+
const newMap = new Map(newTools.map((t) => [t.name, t]));
|
|
8
|
+
for (const [name, oldTool] of oldMap) {
|
|
9
|
+
if (!newMap.has(name)) {
|
|
10
|
+
events.push({
|
|
11
|
+
toolName: name,
|
|
12
|
+
serverName,
|
|
13
|
+
severity: "CRITICAL",
|
|
14
|
+
changeType: "tool_removed",
|
|
15
|
+
message: `Tool "${name}" was removed from server "${serverName}"`,
|
|
16
|
+
});
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const newTool = newMap.get(name);
|
|
20
|
+
events.push(...compareTools(serverName, oldTool, newTool));
|
|
21
|
+
}
|
|
22
|
+
for (const name of newMap.keys()) {
|
|
23
|
+
if (!oldMap.has(name)) {
|
|
24
|
+
events.push({
|
|
25
|
+
toolName: name,
|
|
26
|
+
serverName,
|
|
27
|
+
severity: "WARNING",
|
|
28
|
+
changeType: "tool_added",
|
|
29
|
+
message: `New tool "${name}" appeared on server "${serverName}"`,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return events.sort((a, b) => severityRank(a.severity) - severityRank(b.severity));
|
|
34
|
+
}
|
|
35
|
+
exports.detectDrift = detectDrift;
|
|
36
|
+
function compareTools(serverName, oldTool, newTool) {
|
|
37
|
+
const events = [];
|
|
38
|
+
if (oldTool.description !== newTool.description) {
|
|
39
|
+
events.push({
|
|
40
|
+
toolName: oldTool.name,
|
|
41
|
+
serverName,
|
|
42
|
+
severity: "INFO",
|
|
43
|
+
changeType: "description_changed",
|
|
44
|
+
oldValue: oldTool.description,
|
|
45
|
+
newValue: newTool.description,
|
|
46
|
+
message: `Description changed for tool "${oldTool.name}"`,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const oldProps = getProperties(oldTool.inputSchema);
|
|
50
|
+
const newProps = getProperties(newTool.inputSchema);
|
|
51
|
+
for (const key of Object.keys(oldProps)) {
|
|
52
|
+
if (!(key in newProps)) {
|
|
53
|
+
events.push({
|
|
54
|
+
toolName: oldTool.name,
|
|
55
|
+
serverName,
|
|
56
|
+
severity: "CRITICAL",
|
|
57
|
+
changeType: "parameter_removed",
|
|
58
|
+
field: key,
|
|
59
|
+
oldValue: oldProps[key],
|
|
60
|
+
message: `Parameter "${key}" removed from tool "${oldTool.name}"`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
else if (oldProps[key]?.type !== newProps[key]?.type) {
|
|
64
|
+
events.push({
|
|
65
|
+
toolName: oldTool.name,
|
|
66
|
+
serverName,
|
|
67
|
+
severity: "CRITICAL",
|
|
68
|
+
changeType: "type_changed",
|
|
69
|
+
field: key,
|
|
70
|
+
oldValue: oldProps[key]?.type,
|
|
71
|
+
newValue: newProps[key]?.type,
|
|
72
|
+
message: `Parameter "${key}" type changed from "${oldProps[key]?.type}" to "${newProps[key]?.type}" in tool "${oldTool.name}"`,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const key of Object.keys(newProps)) {
|
|
77
|
+
if (!(key in oldProps)) {
|
|
78
|
+
events.push({
|
|
79
|
+
toolName: oldTool.name,
|
|
80
|
+
serverName,
|
|
81
|
+
severity: "WARNING",
|
|
82
|
+
changeType: "parameter_added",
|
|
83
|
+
field: key,
|
|
84
|
+
newValue: newProps[key],
|
|
85
|
+
message: `Parameter "${key}" added to tool "${oldTool.name}"`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return events;
|
|
90
|
+
}
|
|
91
|
+
function getProperties(schema) {
|
|
92
|
+
if (typeof schema === "object" &&
|
|
93
|
+
schema !== null &&
|
|
94
|
+
"properties" in schema &&
|
|
95
|
+
typeof schema.properties === "object") {
|
|
96
|
+
return schema.properties;
|
|
97
|
+
}
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
function maxSeverity(events) {
|
|
101
|
+
if (events.length === 0)
|
|
102
|
+
return null;
|
|
103
|
+
if (events.some((e) => e.severity === "CRITICAL"))
|
|
104
|
+
return "CRITICAL";
|
|
105
|
+
if (events.some((e) => e.severity === "WARNING"))
|
|
106
|
+
return "WARNING";
|
|
107
|
+
return "INFO";
|
|
108
|
+
}
|
|
109
|
+
exports.maxSeverity = maxSeverity;
|
|
110
|
+
function logDriftEvents(events) {
|
|
111
|
+
for (const e of events) {
|
|
112
|
+
console.error(`[vengtoo-gateway] DRIFT ${e.severity}: ${e.message}`);
|
|
113
|
+
}
|
|
114
|
+
if (events.length > 0) {
|
|
115
|
+
const critical = events.filter((e) => e.severity === "CRITICAL").length;
|
|
116
|
+
const warning = events.filter((e) => e.severity === "WARNING").length;
|
|
117
|
+
const info = events.filter((e) => e.severity === "INFO").length;
|
|
118
|
+
console.error(`[vengtoo-gateway] drift summary: ${critical} critical, ${warning} warning, ${info} info`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
exports.logDriftEvents = logDriftEvents;
|
|
122
|
+
function severityRank(s) {
|
|
123
|
+
switch (s) {
|
|
124
|
+
case "CRITICAL": return 0;
|
|
125
|
+
case "WARNING": return 1;
|
|
126
|
+
case "INFO": return 2;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getOrCreateGatewayId(): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getOrCreateGatewayId = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const path_1 = require("path");
|
|
7
|
+
const VENGTOO_DIR = (0, path_1.join)(process.cwd(), ".vengtoo");
|
|
8
|
+
const ID_FILE = (0, path_1.join)(VENGTOO_DIR, "gateway.id");
|
|
9
|
+
function getOrCreateGatewayId() {
|
|
10
|
+
if ((0, fs_1.existsSync)(ID_FILE)) {
|
|
11
|
+
return (0, fs_1.readFileSync)(ID_FILE, "utf-8").trim();
|
|
12
|
+
}
|
|
13
|
+
(0, fs_1.mkdirSync)(VENGTOO_DIR, { recursive: true });
|
|
14
|
+
const id = `gw_${(0, crypto_1.randomUUID)().replace(/-/g, "").slice(0, 16)}`;
|
|
15
|
+
(0, fs_1.writeFileSync)(ID_FILE, id, "utf-8");
|
|
16
|
+
return id;
|
|
17
|
+
}
|
|
18
|
+
exports.getOrCreateGatewayId = getOrCreateGatewayId;
|