@attackforge/mcp-server 0.2.8 → 0.3.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/README.md +21 -0
- package/dist/index.js +75 -42
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ The server requires the following environment variables:
|
|
|
28
28
|
| `AF_HOSTNAME` | Yes | Your AttackForge server hostname (e.g., `your-instance.attackforge.com`) |
|
|
29
29
|
| `AF_USER_KEY` | Yes | Your AttackForge API user key for authentication |
|
|
30
30
|
| `AF_PORT` | No | Custom port if your AttackForge server uses a non-standard HTTPS port |
|
|
31
|
+
| `AF_TIMEOUT_MS` | No | How long to wait on an AttackForge request before giving up (default `60000`) |
|
|
31
32
|
|
|
32
33
|
## Usage
|
|
33
34
|
|
|
@@ -87,6 +88,26 @@ This MCP server implements the [Model Context Protocol](https://modelcontextprot
|
|
|
87
88
|
|
|
88
89
|
The tools available to Claude are determined by your AttackForge instance configuration.
|
|
89
90
|
|
|
91
|
+
### Protocol versions
|
|
92
|
+
|
|
93
|
+
The server is built on the v2 MCP TypeScript SDK (`@modelcontextprotocol/server`) and serves both protocol eras from the same connection entry point:
|
|
94
|
+
|
|
95
|
+
- **Modern** — the `2026-07-28` revision, negotiated with `server/discover`
|
|
96
|
+
- **Legacy** — the `2024-10-07` through `2025-11-25` revisions, negotiated with the `initialize` handshake
|
|
97
|
+
|
|
98
|
+
The era is decided once per connection and every tool behaves identically on both, so no configuration is needed to support either kind of host.
|
|
99
|
+
|
|
100
|
+
### Errors
|
|
101
|
+
|
|
102
|
+
Failures reported by AttackForge are relayed as MCP protocol errors naming the actual cause:
|
|
103
|
+
|
|
104
|
+
| Status | Meaning |
|
|
105
|
+
|--------|---------|
|
|
106
|
+
| `400` | AttackForge rejected the request; the validation detail is included in the message |
|
|
107
|
+
| `401` | `AF_USER_KEY` is not a valid AttackForge API key |
|
|
108
|
+
| `404` | Local MCP access is disabled on the instance, or the user has no access to the MCP module |
|
|
109
|
+
| other | Relayed with the status and response body |
|
|
110
|
+
|
|
90
111
|
## Development
|
|
91
112
|
|
|
92
113
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { ProtocolError, ProtocolErrorCode, Server } from '@modelcontextprotocol/server';
|
|
3
|
+
import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
|
5
4
|
import { readFile } from 'node:fs/promises';
|
|
6
5
|
if (!process.env.AF_HOSTNAME) {
|
|
7
6
|
throw new Error('Environment variable AF_HOSTNAME is required');
|
|
@@ -9,6 +8,7 @@ if (!process.env.AF_HOSTNAME) {
|
|
|
9
8
|
if (!process.env.AF_USER_KEY) {
|
|
10
9
|
throw new Error('Environment variable AF_USER_KEY is required');
|
|
11
10
|
}
|
|
11
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
12
12
|
let packageJson;
|
|
13
13
|
try {
|
|
14
14
|
const filePath = new URL('../package.json', import.meta.url);
|
|
@@ -18,50 +18,83 @@ try {
|
|
|
18
18
|
catch (err) {
|
|
19
19
|
throw new Error('Failed to parse package.json');
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
function timeoutMs() {
|
|
22
|
+
const configured = Number(process.env.AF_TIMEOUT_MS);
|
|
23
|
+
return (Number.isFinite(configured) && configured > 0) ? configured : DEFAULT_TIMEOUT_MS;
|
|
24
|
+
}
|
|
25
|
+
// The AttackForge endpoints report failures through the HTTP status, so map each
|
|
26
|
+
// one back to a message that names the actual cause instead of a generic error.
|
|
27
|
+
function describeFailure(status, body) {
|
|
28
|
+
switch (status) {
|
|
29
|
+
case 400:
|
|
30
|
+
return `AttackForge rejected the request${body ? `: ${body}` : ''}`;
|
|
31
|
+
case 401:
|
|
32
|
+
return 'AttackForge rejected the credentials - AF_USER_KEY is not a valid API key';
|
|
33
|
+
case 404:
|
|
34
|
+
return 'The AttackForge MCP module is unavailable - local MCP access is disabled on this instance, or this user has no access to the module';
|
|
35
|
+
default:
|
|
36
|
+
return `AttackForge returned ${status}${body ? `: ${body}` : ''}`;
|
|
29
37
|
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const url = new URL(
|
|
38
|
+
}
|
|
39
|
+
async function forward(path, request) {
|
|
40
|
+
const url = new URL(path, `https://${process.env.AF_HOSTNAME}`);
|
|
33
41
|
if (process.env.AF_PORT) {
|
|
34
42
|
url.port = process.env.AF_PORT;
|
|
35
43
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
let response;
|
|
45
|
+
try {
|
|
46
|
+
response = await fetch(url, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
body: JSON.stringify(request),
|
|
49
|
+
headers: {
|
|
50
|
+
'content-type': 'application/json',
|
|
51
|
+
'x-user-key': process.env.AF_USER_KEY,
|
|
52
|
+
},
|
|
53
|
+
signal: AbortSignal.timeout(timeoutMs()),
|
|
54
|
+
});
|
|
46
55
|
}
|
|
47
|
-
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
url.port = process.env.AF_PORT;
|
|
56
|
+
catch (err) {
|
|
57
|
+
throw new ProtocolError(ProtocolErrorCode.InternalError, `Could not reach AttackForge at ${url.host}: ${(err instanceof Error) ? err.message : String(err)}`);
|
|
58
|
+
}
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
throw new ProtocolError(ProtocolErrorCode.InternalError, describeFailure(response.status, (await response.text()).trim()));
|
|
53
61
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
return await response.json();
|
|
63
|
+
}
|
|
64
|
+
// The low-level Server is @deprecated in favour of McpServer, but its note carves
|
|
65
|
+
// out advanced use cases and a pure proxy is one: the tools come from AttackForge
|
|
66
|
+
// at runtime, so there is nothing to registerTool and nothing to derive a schema
|
|
67
|
+
// from. What the low-level path owes the SDK is projectCallToolResult, which
|
|
68
|
+
// McpServer would otherwise call for us - the AttackForge REST endpoints return
|
|
69
|
+
// tool results unprojected, so this proxy is the only place the wire codec runs.
|
|
70
|
+
function createServer() {
|
|
71
|
+
const server = new Server({
|
|
72
|
+
name: packageJson.name,
|
|
73
|
+
version: packageJson.version,
|
|
74
|
+
}, {
|
|
75
|
+
capabilities: {
|
|
76
|
+
tools: {}
|
|
60
77
|
}
|
|
61
78
|
});
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
79
|
+
// tools/list is grant-filtered per principal upstream, so the advertised output
|
|
80
|
+
// schemas belong to this connection and are cached alongside it.
|
|
81
|
+
const outputSchemas = new Map();
|
|
82
|
+
server.setRequestHandler('tools/list', async (request) => {
|
|
83
|
+
const result = await forward('/mcp/tools/list', request);
|
|
84
|
+
for (const tool of result.tools) {
|
|
85
|
+
outputSchemas.set(tool.name, tool.outputSchema);
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
});
|
|
89
|
+
server.setRequestHandler('tools/call', async (request) => {
|
|
90
|
+
const result = await forward('/mcp/tools/call', request);
|
|
91
|
+
return server.projectCallToolResult(result, outputSchemas.get(request.params.name));
|
|
92
|
+
});
|
|
93
|
+
return server;
|
|
94
|
+
}
|
|
95
|
+
const handle = serveStdio(createServer);
|
|
96
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
97
|
+
process.on(signal, () => {
|
|
98
|
+
void handle.close();
|
|
99
|
+
});
|
|
100
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@attackforge/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "AttackForge MCP Server",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -18,13 +18,16 @@
|
|
|
18
18
|
},
|
|
19
19
|
"author": "AttackForge",
|
|
20
20
|
"license": "MIT",
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=22"
|
|
23
|
+
},
|
|
21
24
|
"devDependencies": {
|
|
25
|
+
"@tsconfig/node22": "^22.0.2",
|
|
22
26
|
"@types/node": "^22.16.5",
|
|
23
27
|
"shx": "^0.4.0",
|
|
24
28
|
"typescript": "^6.0.2"
|
|
25
29
|
},
|
|
26
30
|
"dependencies": {
|
|
27
|
-
"@modelcontextprotocol/
|
|
28
|
-
"@tsconfig/node22": "^22.0.2"
|
|
31
|
+
"@modelcontextprotocol/server": "^2.0.0"
|
|
29
32
|
}
|
|
30
33
|
}
|