@mcp-abap-adt/core 4.5.2 → 4.6.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/CHANGELOG.md +5 -0
- package/dist/lib/config/ArgumentsParser.d.ts +6 -0
- package/dist/lib/config/ArgumentsParser.d.ts.map +1 -1
- package/dist/lib/config/ArgumentsParser.js +4 -0
- package/dist/lib/config/ArgumentsParser.js.map +1 -1
- package/dist/lib/config/IServerConfig.d.ts +11 -0
- package/dist/lib/config/IServerConfig.d.ts.map +1 -1
- package/dist/lib/config/ServerConfigManager.d.ts.map +1 -1
- package/dist/lib/config/ServerConfigManager.js +13 -0
- package/dist/lib/config/ServerConfigManager.js.map +1 -1
- package/dist/lib/config/yamlConfig.d.ts +10 -0
- package/dist/lib/config/yamlConfig.d.ts.map +1 -1
- package/dist/lib/config/yamlConfig.js +54 -0
- package/dist/lib/config/yamlConfig.js.map +1 -1
- package/dist/server/SseServer.d.ts +6 -0
- package/dist/server/SseServer.d.ts.map +1 -1
- package/dist/server/SseServer.js +8 -3
- package/dist/server/SseServer.js.map +1 -1
- package/dist/server/StreamableHttpServer.d.ts +6 -0
- package/dist/server/StreamableHttpServer.d.ts.map +1 -1
- package/dist/server/StreamableHttpServer.js +7 -2
- package/dist/server/StreamableHttpServer.js.map +1 -1
- package/dist/server/launcher.js +5 -0
- package/dist/server/launcher.js.map +1 -1
- package/dist/server/tlsUtils.d.ts +19 -0
- package/dist/server/tlsUtils.d.ts.map +1 -0
- package/dist/server/tlsUtils.js +77 -0
- package/dist/server/tlsUtils.js.map +1 -0
- package/docs/superpowers/plans/2026-03-31-https-server-support.md +771 -0
- package/package.json +1 -1
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
# HTTPS Server Support Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Add TLS/HTTPS support to MCP server transports (HTTP and SSE) so clients can connect securely with a provided certificate.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Protocol is determined automatically by the presence of `tls.cert` + `tls.key` options. TLS config is per-transport (http/sse sections) to allow mixed setups. In standalone mode, `https.createServer(tlsOpts, app)` replaces `app.listen()`. Embedded mode ignores TLS (external app's responsibility).
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Node.js `node:https`, `node:fs`, Express 5, existing YAML config + CLI argument infrastructure.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## File Map
|
|
14
|
+
|
|
15
|
+
| File | Action | Responsibility |
|
|
16
|
+
|------|--------|---------------|
|
|
17
|
+
| `src/lib/config/IServerConfig.ts` | Modify | Add `TlsConfig` interface and tls fields |
|
|
18
|
+
| `src/lib/config/ArgumentsParser.ts` | Modify | Parse `--tls-cert`, `--tls-key`, `--tls-ca` CLI args + env vars |
|
|
19
|
+
| `src/lib/config/yamlConfig.ts` | Modify | Add `tls` section to `YamlConfig`, validation, template, apply-to-args |
|
|
20
|
+
| `src/lib/config/ServerConfigManager.ts` | Modify | Wire TLS fields from parsed args to `IServerConfig` |
|
|
21
|
+
| `src/server/tlsUtils.ts` | Create | `createServerListener()` helper — shared by both transports |
|
|
22
|
+
| `src/server/StreamableHttpServer.ts` | Modify | Use `createServerListener()` in `start()`, update log protocol |
|
|
23
|
+
| `src/server/SseServer.ts` | Modify | Use `createServerListener()` in `start()`, update log protocol |
|
|
24
|
+
| `src/server/launcher.ts` | Modify | Pass TLS config to server constructors, update help text |
|
|
25
|
+
| `src/__tests__/unit/tlsServer.test.ts` | Create | Unit tests for TLS server creation |
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
### Task 1: Add TlsConfig to IServerConfig
|
|
30
|
+
|
|
31
|
+
**Files:**
|
|
32
|
+
- Modify: `src/lib/config/IServerConfig.ts`
|
|
33
|
+
|
|
34
|
+
- [ ] **Step 1: Add TlsConfig interface and fields**
|
|
35
|
+
|
|
36
|
+
In `src/lib/config/IServerConfig.ts`, add:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
/** TLS configuration for HTTPS server */
|
|
40
|
+
export interface TlsConfig {
|
|
41
|
+
/** Path to TLS certificate file (PEM) */
|
|
42
|
+
cert: string;
|
|
43
|
+
/** Path to TLS private key file (PEM) */
|
|
44
|
+
key: string;
|
|
45
|
+
/** Path to CA certificate file (PEM), optional */
|
|
46
|
+
ca?: string;
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Add to `IServerConfig` in the `HTTP/SSE SPECIFIC` section:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
/** TLS configuration for HTTPS (cert + key enables HTTPS automatically) */
|
|
54
|
+
tls?: TlsConfig;
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- [ ] **Step 2: Commit**
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
git add src/lib/config/IServerConfig.ts
|
|
61
|
+
git commit -m "feat: add TlsConfig interface to IServerConfig"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
### Task 2: Parse TLS CLI arguments
|
|
67
|
+
|
|
68
|
+
**Files:**
|
|
69
|
+
- Modify: `src/lib/config/ArgumentsParser.ts`
|
|
70
|
+
|
|
71
|
+
- [ ] **Step 1: Add TLS fields to ParsedArguments**
|
|
72
|
+
|
|
73
|
+
Add to `ParsedArguments` interface:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
/** TLS certificate file path */
|
|
77
|
+
tlsCert?: string;
|
|
78
|
+
/** TLS private key file path */
|
|
79
|
+
tlsKey?: string;
|
|
80
|
+
/** TLS CA certificate file path */
|
|
81
|
+
tlsCa?: string;
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- [ ] **Step 2: Add TLS parsing logic**
|
|
85
|
+
|
|
86
|
+
In `ArgumentsParser.parse()`, after the SSE options block (after line ~257), add:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// Parse TLS options
|
|
90
|
+
result.tlsCert = getArgValue('--tls-cert') || process.env.MCP_TLS_CERT;
|
|
91
|
+
result.tlsKey = getArgValue('--tls-key') || process.env.MCP_TLS_KEY;
|
|
92
|
+
result.tlsCa = getArgValue('--tls-ca') || process.env.MCP_TLS_CA;
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
- [ ] **Step 3: Commit**
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
git add src/lib/config/ArgumentsParser.ts
|
|
99
|
+
git commit -m "feat: parse --tls-cert, --tls-key, --tls-ca CLI args"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
### Task 3: Add TLS to YAML config
|
|
105
|
+
|
|
106
|
+
**Files:**
|
|
107
|
+
- Modify: `src/lib/config/yamlConfig.ts`
|
|
108
|
+
|
|
109
|
+
- [ ] **Step 1: Add tls to YamlConfig interface**
|
|
110
|
+
|
|
111
|
+
Add `tls` section inside both `http` and `sse` objects in `YamlConfig`:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
export interface YamlConfig {
|
|
115
|
+
// ... existing fields ...
|
|
116
|
+
http?: {
|
|
117
|
+
// ... existing fields ...
|
|
118
|
+
tls?: {
|
|
119
|
+
cert?: string;
|
|
120
|
+
key?: string;
|
|
121
|
+
ca?: string;
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
sse?: {
|
|
125
|
+
// ... existing fields ...
|
|
126
|
+
tls?: {
|
|
127
|
+
cert?: string;
|
|
128
|
+
key?: string;
|
|
129
|
+
ca?: string;
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
- [ ] **Step 2: Add TLS validation to validateYamlConfig**
|
|
136
|
+
|
|
137
|
+
After existing SSE validation (before the `return` statement), add:
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
// Validate TLS configuration (http)
|
|
141
|
+
if (config.http?.tls) {
|
|
142
|
+
const tls = config.http.tls;
|
|
143
|
+
if (tls.cert && !tls.key) {
|
|
144
|
+
errors.push('HTTP TLS: cert requires key to be specified');
|
|
145
|
+
}
|
|
146
|
+
if (tls.key && !tls.cert) {
|
|
147
|
+
errors.push('HTTP TLS: key requires cert to be specified');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Validate TLS configuration (sse)
|
|
152
|
+
if (config.sse?.tls) {
|
|
153
|
+
const tls = config.sse.tls;
|
|
154
|
+
if (tls.cert && !tls.key) {
|
|
155
|
+
errors.push('SSE TLS: cert requires key to be specified');
|
|
156
|
+
}
|
|
157
|
+
if (tls.key && !tls.cert) {
|
|
158
|
+
errors.push('SSE TLS: key requires cert to be specified');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- [ ] **Step 3: Add TLS to applyYamlConfigToArgs**
|
|
164
|
+
|
|
165
|
+
Inside the `// Apply HTTP options` block, add:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
if (config.http.tls?.cert && !hasArg('--tls-cert')) {
|
|
169
|
+
addArg('--tls-cert', config.http.tls.cert);
|
|
170
|
+
}
|
|
171
|
+
if (config.http.tls?.key && !hasArg('--tls-key')) {
|
|
172
|
+
addArg('--tls-key', config.http.tls.key);
|
|
173
|
+
}
|
|
174
|
+
if (config.http.tls?.ca && !hasArg('--tls-ca')) {
|
|
175
|
+
addArg('--tls-ca', config.http.tls.ca);
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Also inside the `// Apply SSE options` block, add the same (SSE YAML tls applies to same CLI args — both transports share the same tls cert/key/ca since only one transport runs at a time):
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
if (config.sse.tls?.cert && !hasArg('--tls-cert')) {
|
|
183
|
+
addArg('--tls-cert', config.sse.tls.cert);
|
|
184
|
+
}
|
|
185
|
+
if (config.sse.tls?.key && !hasArg('--tls-key')) {
|
|
186
|
+
addArg('--tls-key', config.sse.tls.key);
|
|
187
|
+
}
|
|
188
|
+
if (config.sse.tls?.ca && !hasArg('--tls-ca')) {
|
|
189
|
+
addArg('--tls-ca', config.sse.tls.ca);
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
- [ ] **Step 4: Add TLS to YAML template**
|
|
194
|
+
|
|
195
|
+
In `generateYamlConfigTemplate()`, add to both `http:` and `sse:` sections:
|
|
196
|
+
|
|
197
|
+
```yaml
|
|
198
|
+
# TLS configuration for HTTPS (both cert and key required)
|
|
199
|
+
# When configured, server starts in HTTPS mode automatically
|
|
200
|
+
# Note: only the active transport's TLS config is used
|
|
201
|
+
# tls:
|
|
202
|
+
# cert: /path/to/server.crt
|
|
203
|
+
# key: /path/to/server.key
|
|
204
|
+
# ca: /path/to/ca.crt # optional, for custom CA
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
- [ ] **Step 5: Commit**
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
git add src/lib/config/yamlConfig.ts
|
|
211
|
+
git commit -m "feat: add TLS section to YAML config with validation"
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
### Task 4: Wire TLS config through ServerConfigManager
|
|
217
|
+
|
|
218
|
+
**Files:**
|
|
219
|
+
- Modify: `src/lib/config/ServerConfigManager.ts`
|
|
220
|
+
|
|
221
|
+
- [ ] **Step 1: Add TLS to parseCommandLine**
|
|
222
|
+
|
|
223
|
+
In `parseCommandLine()`, after the existing fields in the return object, add:
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
tls:
|
|
227
|
+
parsed.tlsCert && parsed.tlsKey
|
|
228
|
+
? {
|
|
229
|
+
cert: parsed.tlsCert,
|
|
230
|
+
key: parsed.tlsKey,
|
|
231
|
+
ca: parsed.tlsCa,
|
|
232
|
+
}
|
|
233
|
+
: undefined,
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
- [ ] **Step 2: Add TLS to help text**
|
|
237
|
+
|
|
238
|
+
In `generateHelp()`, add a TLS section to the OPTIONS area. In the help string, after `HTTP OPTIONS:` section, add:
|
|
239
|
+
|
|
240
|
+
```
|
|
241
|
+
TLS/HTTPS:
|
|
242
|
+
--tls-cert=<path> Path to TLS certificate file (PEM)
|
|
243
|
+
--tls-key=<path> Path to TLS private key file (PEM)
|
|
244
|
+
--tls-ca=<path> Path to CA certificate file (PEM, optional)
|
|
245
|
+
When cert and key are provided, server starts in HTTPS mode
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
- [ ] **Step 3: Commit**
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
git add src/lib/config/ServerConfigManager.ts
|
|
252
|
+
git commit -m "feat: wire TLS config from parsed args to IServerConfig"
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
### Task 5: Create TLS server helper
|
|
258
|
+
|
|
259
|
+
**Files:**
|
|
260
|
+
- Create: `src/server/tlsUtils.ts`
|
|
261
|
+
|
|
262
|
+
- [ ] **Step 1: Write the failing test**
|
|
263
|
+
|
|
264
|
+
Create `src/__tests__/unit/tlsServer.test.ts`:
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
import * as fs from 'node:fs';
|
|
268
|
+
import * as https from 'node:https';
|
|
269
|
+
import type { AddressInfo } from 'node:net';
|
|
270
|
+
import express from 'express';
|
|
271
|
+
import { createSelfSignedCert } from './helpers/selfSignedCert';
|
|
272
|
+
import { createServerListener, getProtocol } from '../../server/tlsUtils';
|
|
273
|
+
|
|
274
|
+
let certDir: string;
|
|
275
|
+
let certPath: string;
|
|
276
|
+
let keyPath: string;
|
|
277
|
+
|
|
278
|
+
beforeAll(() => {
|
|
279
|
+
const { cert, key, dir } = createSelfSignedCert();
|
|
280
|
+
certDir = dir;
|
|
281
|
+
certPath = cert;
|
|
282
|
+
keyPath = key;
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
afterAll(() => {
|
|
286
|
+
fs.rmSync(certDir, { recursive: true, force: true });
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe('createServerListener', () => {
|
|
290
|
+
it('creates HTTPS server when tls config is provided', async () => {
|
|
291
|
+
const app = express();
|
|
292
|
+
app.get('/test', (_req, res) => res.json({ ok: true }));
|
|
293
|
+
|
|
294
|
+
const server = createServerListener(app, {
|
|
295
|
+
cert: certPath,
|
|
296
|
+
key: keyPath,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
await new Promise<void>((resolve) => {
|
|
300
|
+
server.listen(0, '127.0.0.1', () => resolve());
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const { port } = server.address() as AddressInfo;
|
|
304
|
+
|
|
305
|
+
// Use node:https to make request with rejectUnauthorized: false
|
|
306
|
+
const body = await new Promise<string>((resolve, reject) => {
|
|
307
|
+
https.get(
|
|
308
|
+
`https://127.0.0.1:${port}/test`,
|
|
309
|
+
{ rejectUnauthorized: false },
|
|
310
|
+
(res) => {
|
|
311
|
+
let data = '';
|
|
312
|
+
res.on('data', (chunk) => (data += chunk));
|
|
313
|
+
res.on('end', () => resolve(data));
|
|
314
|
+
},
|
|
315
|
+
).on('error', reject);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
expect(JSON.parse(body)).toEqual({ ok: true });
|
|
319
|
+
|
|
320
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('creates HTTP server when no tls config is provided', async () => {
|
|
324
|
+
const app = express();
|
|
325
|
+
app.get('/test', (_req, res) => res.json({ ok: true }));
|
|
326
|
+
|
|
327
|
+
const server = createServerListener(app);
|
|
328
|
+
|
|
329
|
+
await new Promise<void>((resolve) => {
|
|
330
|
+
server.listen(0, '127.0.0.1', () => resolve());
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const { port } = server.address() as AddressInfo;
|
|
334
|
+
const res = await fetch(`http://127.0.0.1:${port}/test`);
|
|
335
|
+
expect(res.status).toBe(200);
|
|
336
|
+
|
|
337
|
+
const json = await res.json();
|
|
338
|
+
expect(json.ok).toBe(true);
|
|
339
|
+
|
|
340
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('throws when cert file does not exist', () => {
|
|
344
|
+
const app = express();
|
|
345
|
+
expect(() =>
|
|
346
|
+
createServerListener(app, {
|
|
347
|
+
cert: '/nonexistent/cert.pem',
|
|
348
|
+
key: keyPath,
|
|
349
|
+
}),
|
|
350
|
+
).toThrow(/cert/i);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('throws when key file does not exist', () => {
|
|
354
|
+
const app = express();
|
|
355
|
+
expect(() =>
|
|
356
|
+
createServerListener(app, {
|
|
357
|
+
cert: certPath,
|
|
358
|
+
key: '/nonexistent/key.pem',
|
|
359
|
+
}),
|
|
360
|
+
).toThrow(/key/i);
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
describe('getProtocol', () => {
|
|
365
|
+
it('returns https when tls config provided', () => {
|
|
366
|
+
expect(getProtocol({ cert: 'a', key: 'b' })).toBe('https');
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('returns http when no tls config', () => {
|
|
370
|
+
expect(getProtocol()).toBe('http');
|
|
371
|
+
expect(getProtocol(undefined)).toBe('http');
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Create `src/__tests__/unit/helpers/selfSignedCert.ts`:
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
import { execSync } from 'node:child_process';
|
|
380
|
+
import * as fs from 'node:fs';
|
|
381
|
+
import * as os from 'node:os';
|
|
382
|
+
import * as path from 'node:path';
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Generate a self-signed certificate for testing purposes.
|
|
386
|
+
* Uses openssl CLI (available on Linux/macOS).
|
|
387
|
+
*/
|
|
388
|
+
export function createSelfSignedCert(): {
|
|
389
|
+
cert: string;
|
|
390
|
+
key: string;
|
|
391
|
+
dir: string;
|
|
392
|
+
} {
|
|
393
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-tls-test-'));
|
|
394
|
+
const certPath = path.join(dir, 'server.crt');
|
|
395
|
+
const keyPath = path.join(dir, 'server.key');
|
|
396
|
+
|
|
397
|
+
execSync(
|
|
398
|
+
`openssl req -x509 -newkey rsa:2048 -keyout "${keyPath}" -out "${certPath}" -days 1 -nodes -subj "/CN=localhost"`,
|
|
399
|
+
{ stdio: 'pipe' },
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
return { cert: certPath, key: keyPath, dir };
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
407
|
+
|
|
408
|
+
Run: `npx jest src/__tests__/unit/tlsServer.test.ts --no-coverage`
|
|
409
|
+
Expected: FAIL — `createServerListener` module not found
|
|
410
|
+
|
|
411
|
+
- [ ] **Step 3: Implement createServerListener**
|
|
412
|
+
|
|
413
|
+
Create `src/server/tlsUtils.ts`:
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
import * as fs from 'node:fs';
|
|
417
|
+
import * as http from 'node:http';
|
|
418
|
+
import * as https from 'node:https';
|
|
419
|
+
import type { TlsConfig } from '../lib/config/IServerConfig.js';
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Create HTTP or HTTPS server based on TLS configuration.
|
|
423
|
+
*
|
|
424
|
+
* When `tls` is provided with cert + key paths, creates an HTTPS server.
|
|
425
|
+
* Otherwise creates a plain HTTP server.
|
|
426
|
+
*
|
|
427
|
+
* @param app - Express application (or any http.RequestListener)
|
|
428
|
+
* @param tls - Optional TLS configuration with file paths
|
|
429
|
+
* @returns http.Server or https.Server
|
|
430
|
+
*/
|
|
431
|
+
export function createServerListener(
|
|
432
|
+
app: http.RequestListener,
|
|
433
|
+
tls?: TlsConfig,
|
|
434
|
+
): http.Server | https.Server {
|
|
435
|
+
if (!tls) {
|
|
436
|
+
return http.createServer(app);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (!fs.existsSync(tls.cert)) {
|
|
440
|
+
throw new Error(`TLS cert file not found: ${tls.cert}`);
|
|
441
|
+
}
|
|
442
|
+
if (!fs.existsSync(tls.key)) {
|
|
443
|
+
throw new Error(`TLS key file not found: ${tls.key}`);
|
|
444
|
+
}
|
|
445
|
+
if (tls.ca && !fs.existsSync(tls.ca)) {
|
|
446
|
+
throw new Error(`TLS CA file not found: ${tls.ca}`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const tlsOptions: https.ServerOptions = {
|
|
450
|
+
cert: fs.readFileSync(tls.cert),
|
|
451
|
+
key: fs.readFileSync(tls.key),
|
|
452
|
+
...(tls.ca && { ca: fs.readFileSync(tls.ca) }),
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
return https.createServer(tlsOptions, app);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Returns the protocol string based on TLS configuration.
|
|
460
|
+
*/
|
|
461
|
+
export function getProtocol(tls?: TlsConfig): 'https' | 'http' {
|
|
462
|
+
return tls ? 'https' : 'http';
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
467
|
+
|
|
468
|
+
Run: `npx jest src/__tests__/unit/tlsServer.test.ts --no-coverage`
|
|
469
|
+
Expected: PASS
|
|
470
|
+
|
|
471
|
+
- [ ] **Step 5: Commit**
|
|
472
|
+
|
|
473
|
+
```bash
|
|
474
|
+
git add src/server/tlsUtils.ts src/__tests__/unit/tlsServer.test.ts src/__tests__/unit/helpers/selfSignedCert.ts
|
|
475
|
+
git commit -m "feat: add createServerListener TLS helper with tests"
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
### Task 6: Integrate TLS into StreamableHttpServer
|
|
481
|
+
|
|
482
|
+
**Files:**
|
|
483
|
+
- Modify: `src/server/StreamableHttpServer.ts`
|
|
484
|
+
|
|
485
|
+
- [ ] **Step 1: Add TLS to options and constructor**
|
|
486
|
+
|
|
487
|
+
Add/update imports at the top:
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
import type { Server as HttpServer } from 'node:http';
|
|
491
|
+
import type { Server as HttpsServer } from 'node:https';
|
|
492
|
+
import type { TlsConfig } from '../lib/config/IServerConfig.js';
|
|
493
|
+
import { createServerListener, getProtocol } from './tlsUtils.js';
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
Add to `StreamableHttpServerOptions`:
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
/**
|
|
500
|
+
* TLS configuration for HTTPS mode
|
|
501
|
+
* When cert + key are provided, server starts in HTTPS mode
|
|
502
|
+
*/
|
|
503
|
+
tls?: TlsConfig;
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
Update `standaloneServer` type in the class (line 74):
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
private standaloneServer?: HttpServer | HttpsServer;
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
Add field to the class and set it in constructor:
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
private readonly tls?: TlsConfig;
|
|
516
|
+
// In constructor:
|
|
517
|
+
this.tls = opts?.tls;
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
- [ ] **Step 2: Update start() method**
|
|
521
|
+
|
|
522
|
+
Replace in `start()`:
|
|
523
|
+
|
|
524
|
+
```typescript
|
|
525
|
+
// OLD:
|
|
526
|
+
const server = app.listen(this.port, this.host);
|
|
527
|
+
this.standaloneServer = server;
|
|
528
|
+
|
|
529
|
+
server.once('listening', () => {
|
|
530
|
+
console.error(
|
|
531
|
+
`[StreamableHttpServer] Server started on ${this.host}:${this.port}`,
|
|
532
|
+
);
|
|
533
|
+
console.error(
|
|
534
|
+
`[StreamableHttpServer] Endpoint: http://${this.host}:${this.port}${this.path}`,
|
|
535
|
+
);
|
|
536
|
+
resolve();
|
|
537
|
+
});
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
With:
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
const protocol = getProtocol(this.tls);
|
|
544
|
+
const server = createServerListener(app as any, this.tls);
|
|
545
|
+
this.standaloneServer = server;
|
|
546
|
+
|
|
547
|
+
server.listen(this.port, this.host);
|
|
548
|
+
server.once('listening', () => {
|
|
549
|
+
console.error(
|
|
550
|
+
`[StreamableHttpServer] Server started on ${this.host}:${this.port}`,
|
|
551
|
+
);
|
|
552
|
+
console.error(
|
|
553
|
+
`[StreamableHttpServer] Endpoint: ${protocol}://${this.host}:${this.port}${this.path}`,
|
|
554
|
+
);
|
|
555
|
+
resolve();
|
|
556
|
+
});
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
- [ ] **Step 3: Commit**
|
|
560
|
+
|
|
561
|
+
```bash
|
|
562
|
+
git add src/server/StreamableHttpServer.ts
|
|
563
|
+
git commit -m "feat: integrate TLS into StreamableHttpServer"
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
---
|
|
567
|
+
|
|
568
|
+
### Task 7: Integrate TLS into SseServer
|
|
569
|
+
|
|
570
|
+
**Files:**
|
|
571
|
+
- Modify: `src/server/SseServer.ts`
|
|
572
|
+
|
|
573
|
+
- [ ] **Step 1: Add TLS to options and constructor**
|
|
574
|
+
|
|
575
|
+
Add/update imports at the top:
|
|
576
|
+
|
|
577
|
+
```typescript
|
|
578
|
+
import type { Server as HttpServer } from 'node:http';
|
|
579
|
+
import type { Server as HttpsServer } from 'node:https';
|
|
580
|
+
import type { TlsConfig } from '../lib/config/IServerConfig.js';
|
|
581
|
+
import { createServerListener, getProtocol } from './tlsUtils.js';
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
Add to `SseServerOptions`:
|
|
585
|
+
|
|
586
|
+
```typescript
|
|
587
|
+
/**
|
|
588
|
+
* TLS configuration for HTTPS mode
|
|
589
|
+
*/
|
|
590
|
+
tls?: TlsConfig;
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
Update `standaloneServer` type in the class (line 79):
|
|
594
|
+
|
|
595
|
+
```typescript
|
|
596
|
+
private standaloneServer?: HttpServer | HttpsServer;
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
Add field and set in constructor:
|
|
600
|
+
|
|
601
|
+
```typescript
|
|
602
|
+
private readonly tls?: TlsConfig;
|
|
603
|
+
// In constructor:
|
|
604
|
+
this.tls = opts?.tls;
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
- [ ] **Step 2: Update start() method**
|
|
608
|
+
|
|
609
|
+
Replace in `start()`:
|
|
610
|
+
|
|
611
|
+
```typescript
|
|
612
|
+
// OLD:
|
|
613
|
+
const server = app.listen(this.port, this.host);
|
|
614
|
+
this.standaloneServer = server;
|
|
615
|
+
|
|
616
|
+
server.once('listening', () => {
|
|
617
|
+
console.error(
|
|
618
|
+
`[SseServer] Server started on ${this.host}:${this.port}`,
|
|
619
|
+
);
|
|
620
|
+
console.error(
|
|
621
|
+
`[SseServer] SSE endpoint: http://${this.host}:${this.port}${this.ssePath}`,
|
|
622
|
+
);
|
|
623
|
+
console.error(
|
|
624
|
+
`[SseServer] POST endpoint: http://${this.host}:${this.port}${this.postPath}`,
|
|
625
|
+
);
|
|
626
|
+
resolve();
|
|
627
|
+
});
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
With:
|
|
631
|
+
|
|
632
|
+
```typescript
|
|
633
|
+
const protocol = getProtocol(this.tls);
|
|
634
|
+
const server = createServerListener(app as any, this.tls);
|
|
635
|
+
this.standaloneServer = server;
|
|
636
|
+
|
|
637
|
+
server.listen(this.port, this.host);
|
|
638
|
+
server.once('listening', () => {
|
|
639
|
+
console.error(
|
|
640
|
+
`[SseServer] Server started on ${this.host}:${this.port}`,
|
|
641
|
+
);
|
|
642
|
+
console.error(
|
|
643
|
+
`[SseServer] SSE endpoint: ${protocol}://${this.host}:${this.port}${this.ssePath}`,
|
|
644
|
+
);
|
|
645
|
+
console.error(
|
|
646
|
+
`[SseServer] POST endpoint: ${protocol}://${this.host}:${this.port}${this.postPath}`,
|
|
647
|
+
);
|
|
648
|
+
resolve();
|
|
649
|
+
});
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
- [ ] **Step 3: Commit**
|
|
653
|
+
|
|
654
|
+
```bash
|
|
655
|
+
git add src/server/SseServer.ts
|
|
656
|
+
git commit -m "feat: integrate TLS into SseServer"
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
---
|
|
660
|
+
|
|
661
|
+
### Task 8: Pass TLS config from launcher
|
|
662
|
+
|
|
663
|
+
**Files:**
|
|
664
|
+
- Modify: `src/server/launcher.ts`
|
|
665
|
+
|
|
666
|
+
- [ ] **Step 1: Pass tls to SseServer constructor**
|
|
667
|
+
|
|
668
|
+
In `launcher.ts`, find the SseServer creation block (~line 335) and add `tls`:
|
|
669
|
+
|
|
670
|
+
```typescript
|
|
671
|
+
const server = new SseServer(handlersRegistry, authBrokerFactory, {
|
|
672
|
+
host: config.host,
|
|
673
|
+
port: config.port,
|
|
674
|
+
ssePath: config.ssePath,
|
|
675
|
+
postPath: config.postPath,
|
|
676
|
+
defaultDestination:
|
|
677
|
+
config.mcpDestination ?? (config.envFile ? 'default' : undefined),
|
|
678
|
+
logger: loggerForTransport,
|
|
679
|
+
tls: config.tls, // <-- add this
|
|
680
|
+
});
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
- [ ] **Step 2: Pass tls to StreamableHttpServer constructor**
|
|
684
|
+
|
|
685
|
+
Find the StreamableHttpServer creation block (~line 350) and add `tls`:
|
|
686
|
+
|
|
687
|
+
```typescript
|
|
688
|
+
const server = new StreamableHttpServer(handlersRegistry, authBrokerFactory, {
|
|
689
|
+
host: config.host,
|
|
690
|
+
port: config.port,
|
|
691
|
+
enableJsonResponse: config.httpJsonResponse,
|
|
692
|
+
path: config.httpPath,
|
|
693
|
+
defaultDestination:
|
|
694
|
+
config.mcpDestination ?? (config.envFile ? 'default' : undefined),
|
|
695
|
+
logger: loggerForTransport,
|
|
696
|
+
tls: config.tls, // <-- add this
|
|
697
|
+
});
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
- [ ] **Step 3: Add TLS env vars to V2_HELP_SECTIONS**
|
|
701
|
+
|
|
702
|
+
In `V2_HELP_SECTIONS`, add to the `MCP Server Configuration:` block:
|
|
703
|
+
|
|
704
|
+
```
|
|
705
|
+
MCP_TLS_CERT Path to TLS certificate file (PEM)
|
|
706
|
+
MCP_TLS_KEY Path to TLS private key file (PEM)
|
|
707
|
+
MCP_TLS_CA Path to TLS CA certificate file (PEM, optional)
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
- [ ] **Step 4: Commit**
|
|
711
|
+
|
|
712
|
+
```bash
|
|
713
|
+
git add src/server/launcher.ts
|
|
714
|
+
git commit -m "feat: pass TLS config from launcher to HTTP/SSE servers"
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
---
|
|
718
|
+
|
|
719
|
+
### Task 9: Build and verify
|
|
720
|
+
|
|
721
|
+
- [ ] **Step 1: Run build**
|
|
722
|
+
|
|
723
|
+
Run: `npm run build`
|
|
724
|
+
Expected: No compilation errors
|
|
725
|
+
|
|
726
|
+
- [ ] **Step 2: Run existing unit tests**
|
|
727
|
+
|
|
728
|
+
Run: `npx jest src/__tests__/unit/ --no-coverage`
|
|
729
|
+
Expected: All pass (including new TLS tests and existing health endpoint tests)
|
|
730
|
+
|
|
731
|
+
- [ ] **Step 3: Run linter**
|
|
732
|
+
|
|
733
|
+
Run: `npm run lint`
|
|
734
|
+
Expected: No errors
|
|
735
|
+
|
|
736
|
+
- [ ] **Step 4: Commit any fixes if needed**
|
|
737
|
+
|
|
738
|
+
---
|
|
739
|
+
|
|
740
|
+
## Usage Examples (for documentation reference)
|
|
741
|
+
|
|
742
|
+
```bash
|
|
743
|
+
# CLI with TLS
|
|
744
|
+
mcp-abap-adt --transport=http --port=8443 \
|
|
745
|
+
--tls-cert=/path/to/server.crt \
|
|
746
|
+
--tls-key=/path/to/server.key
|
|
747
|
+
|
|
748
|
+
# CLI with CA chain
|
|
749
|
+
mcp-abap-adt --transport=http --port=8443 \
|
|
750
|
+
--tls-cert=/path/to/server.crt \
|
|
751
|
+
--tls-key=/path/to/server.key \
|
|
752
|
+
--tls-ca=/path/to/ca-chain.crt
|
|
753
|
+
|
|
754
|
+
# Via environment variables
|
|
755
|
+
MCP_TRANSPORT=http MCP_HTTP_PORT=8443 \
|
|
756
|
+
MCP_TLS_CERT=/path/to/server.crt \
|
|
757
|
+
MCP_TLS_KEY=/path/to/server.key \
|
|
758
|
+
mcp-abap-adt
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
```yaml
|
|
762
|
+
# YAML config
|
|
763
|
+
transport: http
|
|
764
|
+
http:
|
|
765
|
+
port: 8443
|
|
766
|
+
host: 0.0.0.0
|
|
767
|
+
tls:
|
|
768
|
+
cert: /path/to/server.crt
|
|
769
|
+
key: /path/to/server.key
|
|
770
|
+
ca: /path/to/ca-chain.crt
|
|
771
|
+
```
|