@ackuity/inline-proxy 0.7.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 +12 -0
- package/README.md +218 -0
- package/dist/common.js +51 -0
- package/dist/config.js +7 -0
- package/dist/inline_engine.js +69 -0
- package/dist/main.js +26 -0
- package/dist/mode.js +208 -0
- package/package.json +26 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Ackuity MCP Proxy - A security-focused Model Context Protocol proxy component for multi-agent AI systems.
|
|
2
|
+
This license applies solely to the MCP Proxy component of the Ackuity AI Security Platform. Other components of the platform may be subject to separate license terms.
|
|
3
|
+
|
|
4
|
+
MIT License
|
|
5
|
+
|
|
6
|
+
Copyright (c) 2026 Ackuity, Inc.
|
|
7
|
+
|
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
11
|
+
|
|
12
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Ackuity Inline MCP Proxy
|
|
2
|
+
|
|
3
|
+
A transparent MCP proxy that intercepts tool calls and evaluates them against the [Ackuity Inline Decision Engine](https://ackuity.ai) before they reach the MCP server. Destructive operations (DELETE, DROP, TRUNCATE) are blocked in real time.
|
|
4
|
+
|
|
5
|
+
## How It Works
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
MCP Client (LLM Agent)
|
|
9
|
+
│
|
|
10
|
+
▼
|
|
11
|
+
┌──────────────────────┐
|
|
12
|
+
│ Ackuity Inline │
|
|
13
|
+
│ MCP Proxy │──── tools/call ──▶ Inline Decision Engine
|
|
14
|
+
│ │◀─── allow/block ──┘
|
|
15
|
+
└──────────────────────┘
|
|
16
|
+
│ (if allowed)
|
|
17
|
+
▼
|
|
18
|
+
MCP Server (e.g. DAB)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The proxy sits between any MCP client and MCP server. When it sees a `tools/call` request, it sends the tool name and arguments to the Ackuity `protect_tools` API. If the decision is **block**, an error is returned to the client and the call never reaches the MCP server.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install
|
|
27
|
+
npm run build
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Configuration
|
|
31
|
+
|
|
32
|
+
Set these environment variables, or add them to a `.env` file in the project root:
|
|
33
|
+
|
|
34
|
+
| Variable | Required | Description | Default |
|
|
35
|
+
|--------------------|----------|--------------------------------------------------|------------------------------------------------------|
|
|
36
|
+
| `INLINE_API_TOKEN` | Yes | Bearer token for the Inline Decision Engine | — |
|
|
37
|
+
| `INLINE_URL` | Yes | Protect tools endpoint URL | — |
|
|
38
|
+
| `INLINE_AGENT_ID` | Yes | Agent ID registered with the Inline Engine | — |
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
The proxy supports three modes depending on how your MCP client and server communicate.
|
|
43
|
+
|
|
44
|
+
### Mode 1: stdio
|
|
45
|
+
|
|
46
|
+
Client connects via stdin/stdout. Proxy spawns the MCP server as a child process.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
node dist/main.js --mode stdio --cmd "dab start --config dab-config.json"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Use this when your MCP client (e.g. Claude Desktop, Cursor) expects to launch a command.
|
|
53
|
+
|
|
54
|
+
### Mode 2: stdio-to-server
|
|
55
|
+
|
|
56
|
+
Client connects via stdin/stdout. Proxy forwards to an already-running HTTP MCP server.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
node dist/main.js --mode stdio-to-server --target http://localhost:5000/mcp
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Use this when your MCP server is already running on HTTP and your client expects stdio.
|
|
63
|
+
|
|
64
|
+
### Mode 3: server
|
|
65
|
+
|
|
66
|
+
Proxy runs as an HTTP server. Both client and MCP server communicate over HTTP.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
node dist/main.js --mode server --target http://localhost:5000/mcp --port 7500
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Use this when your MCP client connects over HTTP (e.g. a web app or remote agent).
|
|
73
|
+
|
|
74
|
+
## How to Use with Your Agent
|
|
75
|
+
|
|
76
|
+
### Option 1: Spawn the proxy from your agent (stdio / stdio-to-server)
|
|
77
|
+
|
|
78
|
+
If your agent uses the MCP SDK, you can launch the proxy as a subprocess using `StdioClientTransport`. The proxy reads Ackuity credentials from environment variables, so pass them via `env`.
|
|
79
|
+
|
|
80
|
+
Replace `<ACKUITY-DIST-REF>` in the examples below depending on how you installed the proxy:
|
|
81
|
+
|
|
82
|
+
| Install method | `command` | `<ACKUITY-DIST-REF>` |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| Cloned and built locally | `node` | `path/to/ackuity-inline-proxy/dist/main.js` |
|
|
85
|
+
| npx from GitHub | `npx` | `github:Ackuity/ackuity-mcp-proxy` |
|
|
86
|
+
| Published to npm | `npx` | `ackuity-inline-proxy` |
|
|
87
|
+
|
|
88
|
+
**TypeScript / JavaScript**
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
92
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
93
|
+
|
|
94
|
+
const transport = new StdioClientTransport({
|
|
95
|
+
command: "npx", // or "node" if running locally
|
|
96
|
+
args: [
|
|
97
|
+
"<ACKUITY-DIST-REF>",
|
|
98
|
+
"--mode", "stdio-to-server",
|
|
99
|
+
"--target", "http://localhost:5000/mcp",
|
|
100
|
+
],
|
|
101
|
+
env: {
|
|
102
|
+
...process.env,
|
|
103
|
+
INLINE_API_TOKEN: "your_token_here",
|
|
104
|
+
INLINE_URL: "https://inline.ackuity.ai/api/v1/inline/protect_tools",
|
|
105
|
+
INLINE_AGENT_ID: "your_agent_id_here",
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const client = new Client({ name: "my-agent", version: "1.0.0" });
|
|
110
|
+
await client.connect(transport);
|
|
111
|
+
|
|
112
|
+
// All tool calls now go through the Ackuity Inline Decision Engine
|
|
113
|
+
const tools = await client.listTools();
|
|
114
|
+
const result = await client.callTool({ name: "read_records", arguments: { entity: "Employees" } });
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
> **Tip:** Use `--mode stdio` with `--cmd` instead if you want the proxy to also spawn the MCP server:
|
|
118
|
+
> ```typescript
|
|
119
|
+
> args: [
|
|
120
|
+
> "<ACKUITY-DIST-REF>",
|
|
121
|
+
> "--mode", "stdio",
|
|
122
|
+
> "--cmd", "npx @azure/data-api-builder start --config dab-config.json",
|
|
123
|
+
> ],
|
|
124
|
+
> ```
|
|
125
|
+
|
|
126
|
+
**Python**
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from mcp.client.stdio import stdio_client, StdioServerParameters
|
|
130
|
+
|
|
131
|
+
server = StdioServerParameters(
|
|
132
|
+
command="npx", # or "node" if running locally
|
|
133
|
+
args=[
|
|
134
|
+
"<ACKUITY-DIST-REF>",
|
|
135
|
+
"--mode", "stdio-to-server",
|
|
136
|
+
"--target", "http://localhost:5000/mcp",
|
|
137
|
+
],
|
|
138
|
+
env={
|
|
139
|
+
**os.environ,
|
|
140
|
+
"INLINE_API_TOKEN": "your_token_here",
|
|
141
|
+
"INLINE_URL": "https://inline.ackuity.ai/api/v1/inline/protect_tools",
|
|
142
|
+
"INLINE_AGENT_ID": "your_agent_id_here",
|
|
143
|
+
},
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
async with stdio_client(server) as (read_stream, write_stream):
|
|
147
|
+
async with ClientSession(read_stream, write_stream) as session:
|
|
148
|
+
await session.initialize()
|
|
149
|
+
result = await session.call_tool("read_records", {"entity": "Employees"})
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Option 2: Run the proxy as a Docker container (server mode)
|
|
153
|
+
|
|
154
|
+
When running in **server mode**, the proxy exposes an HTTP endpoint at `/mcp`. This is ideal for containerized deployments where your agent and MCP server are separate services.
|
|
155
|
+
|
|
156
|
+
**docker-compose.yml**
|
|
157
|
+
|
|
158
|
+
```yaml
|
|
159
|
+
services:
|
|
160
|
+
mcp-server:
|
|
161
|
+
image: your-mcp-server-image
|
|
162
|
+
ports:
|
|
163
|
+
- "5000:5000"
|
|
164
|
+
|
|
165
|
+
ackuity-proxy:
|
|
166
|
+
image: node:22-slim
|
|
167
|
+
working_dir: /app
|
|
168
|
+
command: npx <ACKUITY-DIST-REF> --mode server --target http://mcp-server:5000/mcp --port 7500
|
|
169
|
+
ports:
|
|
170
|
+
- "7500:7500"
|
|
171
|
+
environment:
|
|
172
|
+
INLINE_API_TOKEN: ${INLINE_API_TOKEN}
|
|
173
|
+
INLINE_URL: ${INLINE_URL}
|
|
174
|
+
INLINE_AGENT_ID: ${INLINE_AGENT_ID}
|
|
175
|
+
depends_on:
|
|
176
|
+
- mcp-server
|
|
177
|
+
|
|
178
|
+
agent:
|
|
179
|
+
image: your-agent-image
|
|
180
|
+
environment:
|
|
181
|
+
MCP_ENDPOINT: http://ackuity-proxy:7500/mcp
|
|
182
|
+
depends_on:
|
|
183
|
+
- ackuity-proxy
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Your agent connects to `http://ackuity-proxy:7500/mcp` instead of directly to the MCP server. All tool calls are evaluated by the Ackuity Inline Decision Engine before reaching the MCP server.
|
|
187
|
+
|
|
188
|
+
**With a `.env` file**
|
|
189
|
+
|
|
190
|
+
```env
|
|
191
|
+
INLINE_API_TOKEN=your_token_here
|
|
192
|
+
INLINE_URL=https://inline.ackuity.ai/api/v1/inline/protect_tools
|
|
193
|
+
INLINE_AGENT_ID=your_agent_id_here
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Then run:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
docker compose up
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Fail-Closed Behavior
|
|
203
|
+
|
|
204
|
+
If the Inline Decision Engine is unreachable or returns an error (e.g. invalid token), the proxy **blocks the tool call** rather than forwarding it. This prevents destructive operations from slipping through during outages.
|
|
205
|
+
|
|
206
|
+
## Project Structure
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
src/
|
|
210
|
+
├── main.ts Entry point, arg parsing, mode selection
|
|
211
|
+
├── common.ts Shared utilities (logging, arg parser, constants)
|
|
212
|
+
├── inline_engine.ts Protect Tools API types, response manager, API call
|
|
213
|
+
└── mode.ts ProxyMode interface and implementations (Stdio, StdioToServer, Server)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Logging
|
|
217
|
+
|
|
218
|
+
All MCP traffic is logged to `proxy_traffic.txt` in the working directory. Status messages are written to stderr with a `[PROXY]` prefix.
|
package/dist/common.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
// --- Arg parsing ---
|
|
3
|
+
export class ProxyProcessArguments {
|
|
4
|
+
params = {};
|
|
5
|
+
constructor(args) {
|
|
6
|
+
args.forEach((arg) => {
|
|
7
|
+
const pArg = "--".concat(arg);
|
|
8
|
+
const index = process.argv.indexOf(pArg);
|
|
9
|
+
this.params[arg] = index === -1 ? undefined : process.argv[index + 1];
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
mode() {
|
|
13
|
+
return this.params.mode;
|
|
14
|
+
}
|
|
15
|
+
target() {
|
|
16
|
+
return this.params.target;
|
|
17
|
+
}
|
|
18
|
+
cmd() {
|
|
19
|
+
return this.params.cmd;
|
|
20
|
+
}
|
|
21
|
+
port() {
|
|
22
|
+
return this.params.port;
|
|
23
|
+
}
|
|
24
|
+
toolName() {
|
|
25
|
+
return this.params["tool-name"];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// --- Traffic logging ---
|
|
29
|
+
const logFile = fs.createWriteStream("proxy_traffic.txt", { flags: "a" });
|
|
30
|
+
export function logTraffic(direction, msg) {
|
|
31
|
+
const timestamp = new Date().toISOString();
|
|
32
|
+
const pretty = JSON.stringify(msg, null, 2);
|
|
33
|
+
logFile.write(`\n--- ${direction} ${timestamp} ---\n${pretty}\n`);
|
|
34
|
+
}
|
|
35
|
+
export function logToStderr(text) {
|
|
36
|
+
process.stderr.write(`[PROXY] ${text}\n`);
|
|
37
|
+
}
|
|
38
|
+
// --- Error formatting ---
|
|
39
|
+
export function formatError(err) {
|
|
40
|
+
if (!(err instanceof Error))
|
|
41
|
+
return String(err);
|
|
42
|
+
const cause = err.cause;
|
|
43
|
+
if (cause instanceof AggregateError) {
|
|
44
|
+
const reasons = cause.errors.map((e) => e.message).join("; ");
|
|
45
|
+
return `${err.message} (${reasons})`;
|
|
46
|
+
}
|
|
47
|
+
if (cause instanceof Error) {
|
|
48
|
+
return `${err.message} (${cause.message})`;
|
|
49
|
+
}
|
|
50
|
+
return cause ? `${err.message} (${cause})` : err.message;
|
|
51
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// --- Protect Tools API types ---
|
|
2
|
+
export class ProtectToolsResponseManager {
|
|
3
|
+
request_id;
|
|
4
|
+
session_id;
|
|
5
|
+
request_time;
|
|
6
|
+
response_time;
|
|
7
|
+
status;
|
|
8
|
+
request_evaluation_ms;
|
|
9
|
+
result;
|
|
10
|
+
error;
|
|
11
|
+
error_description;
|
|
12
|
+
constructor(response) {
|
|
13
|
+
Object.assign(this, response);
|
|
14
|
+
}
|
|
15
|
+
isBlocked() {
|
|
16
|
+
return this.result?.decision === "block";
|
|
17
|
+
}
|
|
18
|
+
isAllowed() {
|
|
19
|
+
return this.result?.decision === "allow";
|
|
20
|
+
}
|
|
21
|
+
isError() {
|
|
22
|
+
return this.status === "error";
|
|
23
|
+
}
|
|
24
|
+
getReasons() {
|
|
25
|
+
return this.result?.reason ?? [];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// --- Protect Tools API call ---
|
|
29
|
+
if (!process.env.INLINE_URL) {
|
|
30
|
+
throw new Error("Missing required environment variable: INLINE_URL");
|
|
31
|
+
}
|
|
32
|
+
if (!process.env.INLINE_API_TOKEN) {
|
|
33
|
+
throw new Error("Missing required environment variable: INLINE_API_TOKEN");
|
|
34
|
+
}
|
|
35
|
+
if (!process.env.INLINE_AGENT_ID) {
|
|
36
|
+
throw new Error("Missing required environment variable: INLINE_AGENT_ID");
|
|
37
|
+
}
|
|
38
|
+
const PROTECT_TOOLS_URL = process.env.INLINE_URL;
|
|
39
|
+
const INLINE_API_TOKEN = process.env.INLINE_API_TOKEN;
|
|
40
|
+
const INLINE_AGENT_ID = process.env.INLINE_AGENT_ID;
|
|
41
|
+
let TOOL_NAME = "unknown";
|
|
42
|
+
export function setToolName(name) {
|
|
43
|
+
TOOL_NAME = name;
|
|
44
|
+
}
|
|
45
|
+
export async function callProtectTools(mcpMethod, actionName, toolArguments) {
|
|
46
|
+
const request = {
|
|
47
|
+
messages: [],
|
|
48
|
+
agent: { agent_name: TOOL_NAME, agent_id: INLINE_AGENT_ID },
|
|
49
|
+
tool_calls: [
|
|
50
|
+
{
|
|
51
|
+
tool_name: TOOL_NAME,
|
|
52
|
+
tool_type: "mcp",
|
|
53
|
+
method: mcpMethod,
|
|
54
|
+
action: actionName,
|
|
55
|
+
arguments: toolArguments,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
const res = await fetch(PROTECT_TOOLS_URL, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: {
|
|
62
|
+
Authorization: `Bearer ${INLINE_API_TOKEN}`,
|
|
63
|
+
"Content-Type": "application/json",
|
|
64
|
+
},
|
|
65
|
+
body: JSON.stringify(request),
|
|
66
|
+
});
|
|
67
|
+
const data = await res.json();
|
|
68
|
+
return new ProtectToolsResponseManager(data);
|
|
69
|
+
}
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "dotenv/config";
|
|
3
|
+
import { logToStderr, ProxyProcessArguments } from "./common.js";
|
|
4
|
+
import { modes } from "./config.js";
|
|
5
|
+
import { setToolName } from "./inline_engine.js";
|
|
6
|
+
const programArgs = new ProxyProcessArguments([
|
|
7
|
+
"mode",
|
|
8
|
+
"target",
|
|
9
|
+
"cmd",
|
|
10
|
+
"port",
|
|
11
|
+
"tool-name",
|
|
12
|
+
]);
|
|
13
|
+
const modeName = programArgs.mode();
|
|
14
|
+
const toolName = programArgs.toolName();
|
|
15
|
+
if (toolName) {
|
|
16
|
+
setToolName(toolName);
|
|
17
|
+
}
|
|
18
|
+
logToStderr(`mode: ${modeName}`);
|
|
19
|
+
const ModeClass = modeName ? modes.get(modeName) : undefined;
|
|
20
|
+
if (!ModeClass) {
|
|
21
|
+
logToStderr(`ERROR: --mode required (${[...modes.keys()].join(" | ")})`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
const mode = new ModeClass();
|
|
25
|
+
mode.init(programArgs);
|
|
26
|
+
mode.start();
|
package/dist/mode.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import { createServer, } from "node:http";
|
|
3
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
4
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
7
|
+
import { formatError, logToStderr, logTraffic, } from "./common.js";
|
|
8
|
+
import { DEFAULT_SERVER_PORT } from "./config.js";
|
|
9
|
+
import { callProtectTools } from "./inline_engine.js";
|
|
10
|
+
// --- Interception logic ---
|
|
11
|
+
async function inspectRequest(msg) {
|
|
12
|
+
logTraffic("to", msg);
|
|
13
|
+
if ("method" in msg) {
|
|
14
|
+
logToStderr(`to ${msg.method}`);
|
|
15
|
+
if (msg.method === "tools/call" && msg.params) {
|
|
16
|
+
const params = msg.params;
|
|
17
|
+
logToStderr(` tool: ${params.name}, args: ${JSON.stringify(params.arguments)}`);
|
|
18
|
+
try {
|
|
19
|
+
const decision = await callProtectTools(msg.method, params.name ?? "unknown", params.arguments);
|
|
20
|
+
if (decision.isError()) {
|
|
21
|
+
logToStderr(` protect_tools error: ${decision.error} - ${decision.error_description}`);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
else if (decision.isBlocked()) {
|
|
25
|
+
logToStderr(` BLOCKED: ${decision.getReasons().join(", ")}`);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
logToStderr(` protect_tools call failed: ${formatError(err)}`);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return msg;
|
|
36
|
+
}
|
|
37
|
+
function inspectResponse(msg) {
|
|
38
|
+
logTraffic("from", msg);
|
|
39
|
+
const rpcMsg = msg;
|
|
40
|
+
if ("result" in msg) {
|
|
41
|
+
logToStderr(`from response id:${rpcMsg.id}`);
|
|
42
|
+
}
|
|
43
|
+
else if ("error" in msg) {
|
|
44
|
+
logToStderr(`from error id:${rpcMsg.id}: ${rpcMsg.error?.message}`);
|
|
45
|
+
}
|
|
46
|
+
return msg;
|
|
47
|
+
}
|
|
48
|
+
// --- Wire up two transports ---
|
|
49
|
+
async function startProxy(agentTransport, serverTransport, onClose) {
|
|
50
|
+
let closing = false;
|
|
51
|
+
const closeSession = (reason) => {
|
|
52
|
+
if (closing)
|
|
53
|
+
return;
|
|
54
|
+
closing = true;
|
|
55
|
+
logToStderr(reason);
|
|
56
|
+
agentTransport.close();
|
|
57
|
+
serverTransport.close();
|
|
58
|
+
onClose?.();
|
|
59
|
+
};
|
|
60
|
+
agentTransport.onmessage = async (msg) => {
|
|
61
|
+
const allowed = await inspectRequest(msg);
|
|
62
|
+
if (allowed) {
|
|
63
|
+
await serverTransport.send(allowed);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
if ("id" in msg && msg.id !== undefined) {
|
|
67
|
+
await agentTransport.send({
|
|
68
|
+
jsonrpc: "2.0",
|
|
69
|
+
id: msg.id,
|
|
70
|
+
error: { code: -32001, message: "Blocked by security policy" },
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
serverTransport.onmessage = async (msg) => {
|
|
76
|
+
const allowed = inspectResponse(msg);
|
|
77
|
+
if (allowed) {
|
|
78
|
+
await agentTransport.send(allowed);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
agentTransport.onerror = (err) => logToStderr(`agent error: ${err}`);
|
|
82
|
+
serverTransport.onerror = (err) => logToStderr(`server error: ${err}`);
|
|
83
|
+
agentTransport.onclose = () => closeSession("agent closed");
|
|
84
|
+
serverTransport.onclose = () => closeSession("server closed");
|
|
85
|
+
await serverTransport.start();
|
|
86
|
+
logToStderr("server transport started");
|
|
87
|
+
await agentTransport.start();
|
|
88
|
+
logToStderr("agent transport started");
|
|
89
|
+
}
|
|
90
|
+
// --- Mode: stdio ---
|
|
91
|
+
export class StdioMode {
|
|
92
|
+
cmd;
|
|
93
|
+
init(args) {
|
|
94
|
+
const cmd = args.cmd();
|
|
95
|
+
if (!cmd) {
|
|
96
|
+
logToStderr("ERROR: --cmd required for stdio mode");
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
this.cmd = cmd;
|
|
100
|
+
logToStderr(`cmd: ${this.cmd}`);
|
|
101
|
+
}
|
|
102
|
+
start() {
|
|
103
|
+
const parts = this.cmd.split(" ");
|
|
104
|
+
const agentTransport = new StdioServerTransport();
|
|
105
|
+
const serverTransport = new StdioClientTransport({
|
|
106
|
+
command: parts[0],
|
|
107
|
+
args: parts.slice(1),
|
|
108
|
+
});
|
|
109
|
+
startProxy(agentTransport, serverTransport, () => process.exit(0));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// --- Mode: stdio-to-server ---
|
|
113
|
+
export class StdioToServerMode {
|
|
114
|
+
target;
|
|
115
|
+
init(args) {
|
|
116
|
+
const target = args.target();
|
|
117
|
+
if (!target) {
|
|
118
|
+
logToStderr("ERROR: --target required for stdio-to-server mode");
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
this.target = target;
|
|
122
|
+
logToStderr(`target: ${this.target}`);
|
|
123
|
+
}
|
|
124
|
+
start() {
|
|
125
|
+
const agentTransport = new StdioServerTransport();
|
|
126
|
+
const serverTransport = new StreamableHTTPClientTransport(new URL(this.target));
|
|
127
|
+
startProxy(agentTransport, serverTransport, () => process.exit(0));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// --- Mode: server ---
|
|
131
|
+
export class ServerMode {
|
|
132
|
+
target;
|
|
133
|
+
port;
|
|
134
|
+
init(args) {
|
|
135
|
+
const target = args.target();
|
|
136
|
+
if (!target) {
|
|
137
|
+
logToStderr("ERROR: --target required for server mode");
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
this.target = target;
|
|
141
|
+
this.port = args.port() ?? DEFAULT_SERVER_PORT;
|
|
142
|
+
logToStderr(`starting HTTP server on port ${this.port} to ${this.target}`);
|
|
143
|
+
}
|
|
144
|
+
start() {
|
|
145
|
+
const target = this.target;
|
|
146
|
+
const sessions = new Map();
|
|
147
|
+
const httpServer = createServer(async (req, res) => {
|
|
148
|
+
if (req.url !== "/mcp") {
|
|
149
|
+
res.writeHead(404);
|
|
150
|
+
res.end("Not found");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
154
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
155
|
+
const session = sessions.get(sessionId);
|
|
156
|
+
if (!session)
|
|
157
|
+
return;
|
|
158
|
+
const { agent } = session;
|
|
159
|
+
await agent.handleRequest(req, res);
|
|
160
|
+
}
|
|
161
|
+
else if (req.method === "POST") {
|
|
162
|
+
const agentTransport = new StreamableHTTPServerTransport({
|
|
163
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
164
|
+
});
|
|
165
|
+
const forwardHeaders = {};
|
|
166
|
+
const skipHeaders = [
|
|
167
|
+
"host",
|
|
168
|
+
"content-length",
|
|
169
|
+
"transfer-encoding",
|
|
170
|
+
"connection",
|
|
171
|
+
];
|
|
172
|
+
for (const [key, val] of Object.entries(req.headers)) {
|
|
173
|
+
if (val && !skipHeaders.includes(key)) {
|
|
174
|
+
forwardHeaders[key] = Array.isArray(val) ? val.join(", ") : val;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const serverTransport = new StreamableHTTPClientTransport(new URL(target), {
|
|
178
|
+
requestInit: {
|
|
179
|
+
headers: forwardHeaders,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
await startProxy(agentTransport, serverTransport, () => {
|
|
183
|
+
const sid = agentTransport.sessionId;
|
|
184
|
+
if (sid) {
|
|
185
|
+
sessions.delete(sid);
|
|
186
|
+
logToStderr(`session cleaned up: ${sid}`);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
await agentTransport.handleRequest(req, res);
|
|
190
|
+
const newSessionId = agentTransport.sessionId;
|
|
191
|
+
if (newSessionId) {
|
|
192
|
+
sessions.set(newSessionId, {
|
|
193
|
+
agent: agentTransport,
|
|
194
|
+
server: serverTransport,
|
|
195
|
+
});
|
|
196
|
+
logToStderr(`new session: ${newSessionId}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
res.writeHead(405);
|
|
201
|
+
res.end("Method not allowed");
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
httpServer.listen(Number(this.port), () => {
|
|
205
|
+
logToStderr(`listening on http://localhost:${this.port}/mcp`);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ackuity/inline-proxy",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/main.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ackuity-mcp-proxy": "dist/main.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc && npx biome check src/",
|
|
12
|
+
"prepare": "tsc",
|
|
13
|
+
"start": "node dist/main.js",
|
|
14
|
+
"lint": "npx biome check src/",
|
|
15
|
+
"lint:fix": "npx biome check --write src/"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
19
|
+
"dotenv": "^16.4.7"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@biomejs/biome": "^2.4.9",
|
|
23
|
+
"@types/node": "^25.4.0",
|
|
24
|
+
"typescript": "^5.9.3"
|
|
25
|
+
}
|
|
26
|
+
}
|