@winstonsayno/mcp-gateway 3.7.0 → 3.9.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +20 -0
  3. package/dashboard/portal.html +202 -0
  4. package/dist/bench/index.d.ts +50 -0
  5. package/dist/bench/index.d.ts.map +1 -0
  6. package/dist/bench/index.js +162 -0
  7. package/dist/bench/index.js.map +1 -0
  8. package/dist/cli.js +77 -2
  9. package/dist/cli.js.map +1 -1
  10. package/dist/config/loader.d.ts +4 -4
  11. package/dist/config/loader.d.ts.map +1 -1
  12. package/dist/config/loader.js +29 -3
  13. package/dist/config/loader.js.map +1 -1
  14. package/dist/config/migrate.d.ts +30 -0
  15. package/dist/config/migrate.d.ts.map +1 -0
  16. package/dist/config/migrate.js +90 -0
  17. package/dist/config/migrate.js.map +1 -0
  18. package/dist/gateway/admin.d.ts +1 -1
  19. package/dist/gateway/admin.js +1 -1
  20. package/dist/gateway/api.d.ts +3 -0
  21. package/dist/gateway/api.d.ts.map +1 -1
  22. package/dist/gateway/api.js +136 -0
  23. package/dist/gateway/api.js.map +1 -1
  24. package/dist/gateway/index.d.ts +5 -0
  25. package/dist/gateway/index.d.ts.map +1 -1
  26. package/dist/gateway/index.js +28 -2
  27. package/dist/gateway/index.js.map +1 -1
  28. package/dist/index.d.ts +7 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +3 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/portal/index.d.ts +90 -0
  33. package/dist/portal/index.d.ts.map +1 -0
  34. package/dist/portal/index.js +223 -0
  35. package/dist/portal/index.js.map +1 -0
  36. package/dist/utils/deprecations.d.ts +27 -2
  37. package/dist/utils/deprecations.d.ts.map +1 -1
  38. package/dist/utils/deprecations.js +56 -5
  39. package/dist/utils/deprecations.js.map +1 -1
  40. package/dist/utils/types.d.ts +28 -0
  41. package/dist/utils/types.d.ts.map +1 -1
  42. package/package.json +2 -1
package/CHANGELOG.md CHANGED
@@ -9,6 +9,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [3.9.0] - 2026-10-08
13
+
14
+ ### Added
15
+ - **`mcp-gateway migrate`** rewrites a config file to schema v4 — `version: 4`, API-key scope nested under `scope:`,
16
+ `least-latency` → `smart` with a latency-only score — keeping YAML comments and layout. `--write` (keeps a `.bak`),
17
+ `-o <file>`, `--check` (exit 3 when a migration is needed, for CI).
18
+ - **v4 preview:** 3.9 already reads `version: 4` and `auth.apiKeys[].scope`, so a migrated file runs on 3.9 before
19
+ the 4.0 upgrade.
20
+ - **`mcp-gateway bench`**: in-process benchmark (REST, REST with auth, cache hit, `/mcp`) with req/s and p50 / p95 / p99;
21
+ `runBenchmark()` exported. Reference numbers in [docs/benchmarks.md](docs/benchmarks.md).
22
+ - [Migrating to 4.0](docs/guides/migrating-to-v4.md) (preview). The GitHub Pages demo lists the new deprecations.
23
+
24
+ ### Deprecated (removed in 4.0)
25
+ - `version: 3` → `version: 4`.
26
+ - `servers` / `tools` / `rateLimit` directly on an API key → `scope: { servers, tools, rateLimit }`.
27
+ - `loadBalancing.strategy: least-latency` → `smart` with `score: { latency: 1, errorRate: 0, cost: 0 }`.
28
+ - (Already scheduled) plugin API v1.
29
+
30
+ `mcp-gateway validate` and `GET /api/v1/admin/deprecations` list each with the affected keys / servers.
31
+
32
+ ## [3.8.0] - 2026-10-08
33
+
34
+ ### Added
35
+ - **Developer portal** at `/portal` (`portal.enabled`, needs `auth.strategy: api-key`): self-service API keys
36
+ (`signup: open | approval | closed`, allowed e-mail domains, keys per e-mail, default scope / rate limit / TTL),
37
+ shown once and stored as SHA-256 digests (`portal.keysFile` to persist).
38
+ - **Usage view:** a key holder sees 7-day calls, errors, latency, calls per tool and per day; can rotate or revoke
39
+ the key.
40
+ - **Interactive tool docs:** every tool in the key's scope with its input schema, generated example arguments,
41
+ curl / JavaScript / Python snippets and a live **Try it**.
42
+ - Portal keys join `auth.apiKeys` as `portal-<id>`, so scopes, quotas, policy, audit and compliance apply unchanged.
43
+ Operators approve / deny / revoke via `/api/v1/portal/keys`.
44
+ - The GitHub Pages demo publishes `portal.html` with a simulated backend; `dashboard/portal.html` ships in the npm
45
+ package and the Docker image.
46
+
47
+ ### Security
48
+ - Self-contained page with a hash-based CSP (no inline handlers, no CDN); signups throttled per IP; key records
49
+ never expose digests.
50
+
12
51
  ## [3.7.0] - 2026-10-08
13
52
 
14
53
  ### Added
package/README.md CHANGED
@@ -581,6 +581,26 @@ await gateway.start();
581
581
  process.on('SIGTERM', () => gateway.stop());
582
582
  ```
583
583
 
584
+ ## What's New in v3.9
585
+
586
+ | Feature | Description |
587
+ |---------|-------------|
588
+ | **`mcp-gateway migrate`** | Rewrites your config to schema v4, comments kept; `--check` for CI |
589
+ | **v4 deprecation warnings** | `version: 3`, flat API-key scope, `least-latency` — removed in 4.0 |
590
+ | **`mcp-gateway bench`** | Built-in load test with latency percentiles; see [benchmarks](docs/benchmarks.md) |
591
+
592
+ Details: [CHANGELOG](CHANGELOG.md) · [Migrating to 4.0](docs/guides/migrating-to-v4.md).
593
+
594
+ ## What's New in v3.8
595
+
596
+ | Feature | Description |
597
+ |---------|-------------|
598
+ | **Self-serve keys** | `/portal`: sign up, get a scoped key; open, approval or closed signup |
599
+ | **Usage** | 7-day calls, errors, latency per key; rotate / revoke |
600
+ | **Interactive docs** | Tools in scope with schemas, example arguments, snippets and “Try it” |
601
+
602
+ Details: [CHANGELOG](CHANGELOG.md) · [Configuration](docs/configuration.md#developer-portal-38).
603
+
584
604
  ## What's New in v3.7
585
605
 
586
606
  | Feature | Description |
@@ -0,0 +1,202 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
6
+ <meta name="color-scheme" content="dark light" />
7
+ <meta name="theme-color" content="#0b0d12" />
8
+ <title>mcp-gateway · Developer portal</title>
9
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%236d5efc'/%3E%3Cpath d='M9 22V10l7 7 7-7v12' stroke='white' stroke-width='2.6' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E" />
10
+ <!--
11
+ mcp-gateway developer portal (3.8) — one self-contained file like the dashboard: no build step, no CDN.
12
+ Talks only to /api/v1/portal/* and /api/v1/tools/call. Served at /portal when portal.enabled.
13
+ -->
14
+ <style>
15
+ :root { --radius: 14px; --font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif; --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; --accent: #7c6cff; --ok: #22c55e; --bad: #ef4444; --warn: #f59e0b;
16
+ --bg: #0b0d12; --surface: #161a24; --surface-2: #1c2130; --border: rgba(255,255,255,.09); --text: #e8ebf3; --muted: #9aa3b5; }
17
+ @media (prefers-color-scheme: light) { :root { --bg: #f6f7fb; --surface: #fff; --surface-2: #f0f2f8; --border: rgba(10,20,40,.1); --text: #141822; --muted: #5b6475; } }
18
+ * { box-sizing: border-box; }
19
+ body { margin: 0; font: 15px/1.5 var(--font); background: var(--bg); color: var(--text); }
20
+ header { max-width: 980px; margin: 0 auto; padding: 28px 20px 8px; display: flex; gap: 14px; align-items: center; flex-wrap: wrap; }
21
+ header h1 { font-size: 22px; margin: 0; letter-spacing: -.01em; }
22
+ header .ver { color: var(--muted); font-size: 13px; }
23
+ header nav { margin-left: auto; display: flex; gap: 6px; }
24
+ main { max-width: 980px; margin: 0 auto; padding: 8px 20px 60px; display: grid; gap: 16px; }
25
+ .card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 18px 20px; min-width: 0; }
26
+ .card h2 { font-size: 16px; margin: 0 0 10px; }
27
+ .muted { color: var(--muted); font-size: 13.5px; }
28
+ label { display: block; font-size: 13px; color: var(--muted); margin: 10px 0 4px; }
29
+ input, textarea { width: 100%; font: inherit; color: var(--text); background: var(--surface-2); border: 1px solid var(--border); border-radius: 9px; padding: 9px 11px; }
30
+ textarea { font: 13px/1.45 var(--mono); min-height: 96px; resize: vertical; }
31
+ button { font: 600 14px var(--font); border: 0; border-radius: 9px; padding: 9px 14px; cursor: pointer; background: var(--accent); color: #fff; }
32
+ button.ghost { background: transparent; color: var(--text); border: 1px solid var(--border); }
33
+ button.danger { background: transparent; color: var(--bad); border: 1px solid var(--bad); }
34
+ button:disabled { opacity: .5; cursor: default; }
35
+ .row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-top: 12px; }
36
+ .grid2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px; }
37
+ .key { font: 600 14px var(--mono); word-break: break-all; padding: 10px 12px; border-radius: 9px; background: var(--surface-2); border: 1px dashed var(--accent); }
38
+ .badge { display: inline-block; font-size: 12px; padding: 2px 8px; border-radius: 99px; border: 1px solid var(--border); }
39
+ .badge.active { color: var(--ok); border-color: var(--ok); }
40
+ .badge.pending { color: var(--warn); border-color: var(--warn); }
41
+ .stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin: 8px 0; }
42
+ .stat b { display: block; font-size: 22px; }
43
+ .bars { display: flex; align-items: flex-end; gap: 4px; height: 60px; margin-top: 8px; }
44
+ .bars span { flex: 1; background: var(--accent); border-radius: 4px 4px 0 0; min-height: 2px; }
45
+ .tools { display: grid; gap: 10px; }
46
+ details.tool { background: var(--surface-2); border: 1px solid var(--border); border-radius: 11px; padding: 10px 14px; }
47
+ details.tool summary { cursor: pointer; font-weight: 600; list-style: none; display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }
48
+ details.tool summary small { color: var(--muted); font-weight: 400; }
49
+ pre { font: 12.5px/1.45 var(--mono); background: var(--bg); border: 1px solid var(--border); border-radius: 9px; padding: 10px 12px; overflow: auto; margin: 8px 0; white-space: pre-wrap; word-break: break-word; }
50
+ .tabs { display: flex; gap: 6px; margin-top: 8px; }
51
+ .tabs button { padding: 4px 10px; font-size: 12.5px; }
52
+ .msg { margin-top: 10px; font-size: 13.5px; }
53
+ .msg.err { color: var(--bad); }
54
+ .hide { display: none !important; }
55
+ @media (max-width: 560px) { header nav { margin-left: 0; } .stats { grid-template-columns: 1fr 1fr 1fr; } }
56
+ </style>
57
+ </head>
58
+ <body>
59
+ <header>
60
+ <h1 id="title">Developer portal</h1><span class="ver" id="ver"></span>
61
+ <nav><button class="ghost" id="lang" type="button">中文</button></nav>
62
+ </header>
63
+ <main>
64
+ <div class="grid2">
65
+ <section class="card" id="signup-card">
66
+ <h2 data-t="signup">Get an API key</h2>
67
+ <p class="muted" id="signup-note"></p>
68
+ <form id="signup">
69
+ <label for="name" data-t="name">Name or project</label>
70
+ <input id="name" name="name" required maxlength="80" autocomplete="name" />
71
+ <label for="email" data-t="email">E-mail</label>
72
+ <input id="email" name="email" type="email" required autocomplete="email" />
73
+ <div class="row"><button type="submit" data-t="create">Create key</button></div>
74
+ </form>
75
+ <div id="issued" class="hide">
76
+ <p class="msg" id="issued-msg"></p>
77
+ <div class="key" id="issued-key"></div>
78
+ <div class="row"><button class="ghost" id="copy" type="button" data-t="copy">Copy</button><button class="ghost" id="use" type="button" data-t="use">Use this key here</button></div>
79
+ </div>
80
+ <p class="msg err hide" id="signup-err"></p>
81
+ </section>
82
+ <section class="card">
83
+ <h2 data-t="mykey">Your key</h2>
84
+ <label for="key" data-t="paste">Paste your API key (kept in this tab only)</label>
85
+ <input id="key" type="password" autocomplete="off" spellcheck="false" />
86
+ <div class="row"><button id="load" type="button" data-t="load">Load usage &amp; tools</button><button class="ghost hide" id="rotate" type="button" data-t="rotate">Rotate</button><button class="danger hide" id="revoke" type="button" data-t="revoke">Revoke</button></div>
87
+ <div id="me" class="hide">
88
+ <p><span id="me-name"></span> <span class="badge" id="me-status"></span></p>
89
+ <div class="stats"><div class="stat"><b id="u-calls">0</b><span class="muted" data-t="calls">calls (7 days)</span></div><div class="stat"><b id="u-err">0</b><span class="muted" data-t="errors">errors</span></div><div class="stat"><b id="u-lat">0</b><span class="muted" data-t="latency">avg ms</span></div></div>
90
+ <div class="bars" id="bars" aria-label="Calls per day"></div>
91
+ <p class="muted" id="me-scope"></p>
92
+ </div>
93
+ <p class="msg err hide" id="me-err"></p>
94
+ </section>
95
+ </div>
96
+ <section class="card">
97
+ <h2 data-t="tools">Tools you can call</h2>
98
+ <p class="muted" data-t="toolsnote">Load your key to see the tools in its scope, with example arguments, snippets and a live “Try it”.</p>
99
+ <div class="tools" id="tools"></div>
100
+ </section>
101
+ </main>
102
+ <script>
103
+ (() => {
104
+ 'use strict';
105
+ const T = {
106
+ en: { signup: 'Get an API key', name: 'Name or project', email: 'E-mail', create: 'Create key', copy: 'Copy', use: 'Use this key here', mykey: 'Your key', paste: 'Paste your API key (kept in this tab only)', load: 'Load usage & tools', rotate: 'Rotate', revoke: 'Revoke', calls: 'calls (7 days)', errors: 'errors', latency: 'avg ms', tools: 'Tools you can call', toolsnote: 'Load your key to see the tools in its scope, with example arguments, snippets and a live “Try it”.', open: 'Keys work immediately.', approval: 'An operator approves new keys before they work.', closed: 'Self-service signup is closed — ask an operator for a key.', domains: 'Allowed e-mail domains: ', tryit: 'Try it', args: 'Arguments (JSON)', copied: 'Copied', confirmRevoke: 'Revoke this key? Apps using it stop working.', scope: 'Scope: ', none: 'No tools in this key’s scope yet.', expires: 'expires ' },
107
+ zh: { signup: '申请 API Key', name: '名称或项目', email: '邮箱', create: '创建 Key', copy: '复制', use: '在此页使用该 Key', mykey: '我的 Key', paste: '粘贴你的 API Key(仅保存在本标签页)', load: '加载用量与工具', rotate: '轮换', revoke: '吊销', calls: '次调用(7 天)', errors: '错误', latency: '平均毫秒', tools: '可调用的工具', toolsnote: '加载 Key 后可查看其权限范围内的工具、示例参数、代码片段,并可在线试用。', open: '新 Key 立即可用。', approval: '新 Key 需管理员审批后才能使用。', closed: '自助申请已关闭,请联系管理员获取 Key。', domains: '允许的邮箱域名:', tryit: '试一试', args: '参数(JSON)', copied: '已复制', confirmRevoke: '确定吊销该 Key?使用它的应用将无法访问。', scope: '范围:', none: '该 Key 的范围内暂无工具。', expires: '到期 ' },
108
+ };
109
+ let lang = (localStorage.getItem('mcp-gateway.lang') || navigator.language || 'en').startsWith('zh') ? 'zh' : 'en';
110
+ const t = (k) => T[lang][k] || T.en[k] || k;
111
+ const $ = (id) => document.getElementById(id);
112
+ const api = (p, init = {}) => {
113
+ const key = sessionStorage.getItem('mcp-gateway.portal.key');
114
+ const headers = { 'Content-Type': 'application/json', ...(init.auth !== false && key ? { Authorization: 'Bearer ' + key } : {}) };
115
+ return fetch('api/v1' + p, { ...init, headers }).then(async (r) => { const b = await r.json().catch(() => ({})); if (!r.ok) throw new Error(b.message || b.error || ('HTTP ' + r.status)); return b; });
116
+ };
117
+ const show = (el, on) => el.classList.toggle('hide', !on);
118
+ const err = (el, e) => { el.textContent = e ? String(e.message || e) : ''; show(el, !!e); };
119
+ let info = null;
120
+ function applyLang() {
121
+ document.documentElement.lang = lang === 'zh' ? 'zh-CN' : 'en';
122
+ document.querySelectorAll('[data-t]').forEach((el) => { el.textContent = t(el.dataset.t); });
123
+ $('lang').textContent = lang === 'zh' ? 'English' : '中文';
124
+ if (info) $('signup-note').textContent = t(info.signup) + (info.allowedEmailDomains.length ? ' ' + t('domains') + info.allowedEmailDomains.map((d) => '@' + d).join(', ') : '');
125
+ }
126
+ $('lang').addEventListener('click', () => { lang = lang === 'zh' ? 'en' : 'zh'; localStorage.setItem('mcp-gateway.lang', lang); applyLang(); });
127
+ api('/portal/info', { auth: false }).then((i) => {
128
+ info = i;
129
+ $('title').textContent = i.title;
130
+ $('ver').textContent = 'v' + i.version;
131
+ if (i.signup === 'closed') show($('signup'), false);
132
+ applyLang();
133
+ }).catch((e) => err($('signup-err'), e));
134
+
135
+ $('signup').addEventListener('submit', (ev) => {
136
+ ev.preventDefault();
137
+ err($('signup-err'));
138
+ api('/portal/signup', { method: 'POST', auth: false, body: JSON.stringify({ name: $('name').value, email: $('email').value }) }).then((r) => {
139
+ $('issued-msg').textContent = r.message;
140
+ $('issued-key').textContent = r.key;
141
+ show($('issued'), true);
142
+ show($('signup'), false);
143
+ }).catch((e) => err($('signup-err'), e));
144
+ });
145
+ $('copy').addEventListener('click', () => { navigator.clipboard?.writeText($('issued-key').textContent).then(() => { $('copy').textContent = t('copied'); }); });
146
+ $('use').addEventListener('click', () => { $('key').value = $('issued-key').textContent; load(); });
147
+ $('load').addEventListener('click', load);
148
+ $('key').addEventListener('keydown', (e) => { if (e.key === 'Enter') load(); });
149
+
150
+ function load() {
151
+ const k = $('key').value.trim();
152
+ if (!k) return;
153
+ sessionStorage.setItem('mcp-gateway.portal.key', k);
154
+ err($('me-err'));
155
+ api('/portal/me').then(renderMe).catch((e) => { show($('me'), false); err($('me-err'), e); });
156
+ api('/portal/tools').then(renderTools).catch(() => {});
157
+ }
158
+ function renderMe(m) {
159
+ show($('me'), true); show($('rotate'), true); show($('revoke'), true);
160
+ $('me-name').textContent = m.key.name + ' · ' + m.key.prefix + '…';
161
+ $('me-status').textContent = m.key.status;
162
+ $('me-status').className = 'badge ' + m.key.status;
163
+ $('u-calls').textContent = m.usage.calls;
164
+ $('u-err').textContent = m.usage.errors;
165
+ $('u-lat').textContent = m.usage.avgLatencyMs;
166
+ const max = Math.max(1, ...m.usage.byDay.map((d) => d.calls));
167
+ $('bars').replaceChildren(...m.usage.byDay.map((d) => { const s = document.createElement('span'); s.style.height = Math.round((d.calls / max) * 100) + '%'; s.title = d.day + ': ' + d.calls; return s; }));
168
+ $('me-scope').textContent = t('scope') + ((m.key.servers || ['*']).join(', ')) + (m.key.rateLimit ? ' · ' + m.key.rateLimit.limit + '/' + m.key.rateLimit.windowSeconds + 's' : '') + (m.key.expiresAt ? ' · ' + t('expires') + m.key.expiresAt.slice(0, 10) : '');
169
+ }
170
+ $('rotate').addEventListener('click', () => api('/portal/me/rotate', { method: 'POST' }).then((r) => { $('key').value = r.key; $('issued-msg').textContent = r.message; $('issued-key').textContent = r.key; show($('issued'), true); load(); }).catch((e) => err($('me-err'), e)));
171
+ $('revoke').addEventListener('click', () => { if (confirm(t('confirmRevoke'))) api('/portal/me', { method: 'DELETE' }).then(() => { sessionStorage.removeItem('mcp-gateway.portal.key'); $('key').value = ''; show($('me'), false); $('tools').replaceChildren(); }).catch((e) => err($('me-err'), e)); });
172
+
173
+ function el(tag, props = {}, ...kids) { const e = document.createElement(tag); Object.assign(e, props); e.append(...kids); return e; }
174
+ function renderTools(r) {
175
+ const box = $('tools');
176
+ if (!r.tools.length) return box.replaceChildren(el('p', { className: 'muted', textContent: t('none') }));
177
+ box.replaceChildren(...r.tools.map((tool) => {
178
+ const pre = el('pre', { textContent: tool.snippets.curl });
179
+ const tabs = el('div', { className: 'tabs' }, ...['curl', 'javascript', 'python'].map((lang2) => { const b = el('button', { type: 'button', className: 'ghost', textContent: lang2 }); b.addEventListener('click', () => { pre.textContent = tool.snippets[lang2]; }); return b; }));
180
+ const args = el('textarea', { value: JSON.stringify(tool.example, null, 2), spellcheck: false });
181
+ const out = el('pre', { className: 'hide' });
182
+ const run = el('button', { type: 'button', textContent: t('tryit') });
183
+ run.addEventListener('click', () => {
184
+ let a; try { a = JSON.parse(args.value || '{}'); } catch (e) { out.textContent = String(e.message); show(out, true); return; }
185
+ run.disabled = true;
186
+ api('/tools/call', { method: 'POST', body: JSON.stringify({ server: tool.server, tool: tool.name, arguments: a }) }).then((res) => { out.textContent = JSON.stringify(res.result, null, 2); }).catch((e) => { out.textContent = String(e.message); }).finally(() => { run.disabled = false; show(out, true); });
187
+ });
188
+ return el('details', { className: 'tool' },
189
+ el('summary', {}, tool.server + ' / ' + tool.name, el('small', { textContent: tool.description || '' })),
190
+ el('pre', { textContent: JSON.stringify(tool.inputSchema || {}, null, 2) }),
191
+ tabs, pre,
192
+ el('label', { textContent: t('args') }), args,
193
+ el('div', { className: 'row' }, run), out);
194
+ }));
195
+ }
196
+ applyLang();
197
+ const saved = sessionStorage.getItem('mcp-gateway.portal.key');
198
+ if (saved) { $('key').value = saved; load(); }
199
+ })();
200
+ </script>
201
+ </body>
202
+ </html>
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Built-in benchmark (3.9): `mcp-gateway bench`.
3
+ *
4
+ * Starts an in-process gateway on 127.0.0.1 with a minimal stdio echo MCP server (written to a temp file, run with the
5
+ * current Node binary) and drives it with N concurrent keep-alive clients for a fixed time per scenario:
6
+ *
7
+ * - `rest`: `POST /api/v1/tools/call` (auth off) — the full pipeline (scopes, policy, quotas, audit in memory);
8
+ * - `rest-auth`: the same with an API key and a rate-limit-free key;
9
+ * - `cache`: a cached tool (`cache.rules`) — the gateway's own overhead without the upstream round trip;
10
+ * - `mcp`: JSON-RPC `tools/call` on the streamable-HTTP `/mcp` endpoint (one session per client).
11
+ *
12
+ * Reports requests/s, mean and p50 / p95 / p99 latency, and errors. Numbers depend on the machine; compare runs on
13
+ * the same host.
14
+ *
15
+ * @module bench
16
+ */
17
+ export type BenchScenario = 'rest' | 'rest-auth' | 'cache' | 'mcp';
18
+ export declare const BENCH_SCENARIOS: BenchScenario[];
19
+ export interface BenchOptions {
20
+ durationMs?: number;
21
+ concurrency?: number;
22
+ scenarios?: BenchScenario[];
23
+ /** Warm-up per scenario (not measured). */
24
+ warmupMs?: number;
25
+ }
26
+ export interface BenchResult {
27
+ scenario: BenchScenario;
28
+ requests: number;
29
+ errors: number;
30
+ rps: number;
31
+ meanMs: number;
32
+ p50Ms: number;
33
+ p95Ms: number;
34
+ p99Ms: number;
35
+ maxMs: number;
36
+ }
37
+ export interface BenchReport {
38
+ node: string;
39
+ platform: string;
40
+ cpus: number;
41
+ concurrency: number;
42
+ durationMs: number;
43
+ results: BenchResult[];
44
+ }
45
+ export declare function percentile(sorted: number[], p: number): number;
46
+ export declare function summarize(scenario: BenchScenario, latencies: number[], errors: number, elapsedMs: number): BenchResult;
47
+ export declare function runBenchmark(opts?: BenchOptions): Promise<BenchReport>;
48
+ /** Markdown table of a report. */
49
+ export declare function benchMarkdown(r: BenchReport): string;
50
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/bench/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAQH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,WAAW,GAAG,OAAO,GAAG,KAAK,CAAC;AACnE,eAAO,MAAM,eAAe,EAAE,aAAa,EAA0C,CAAC;AAEtF,MAAM,WAAW,YAAY;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,aAAa,EAAE,CAAC;IAC5B,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,aAAa,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,WAAW,EAAE,CAAC;CACxB;AAeD,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAI9D;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,CActH;AAyBD,wBAAsB,YAAY,CAAC,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAwEhF;AAED,kCAAkC;AAClC,wBAAgB,aAAa,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,CAIpD"}
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Built-in benchmark (3.9): `mcp-gateway bench`.
3
+ *
4
+ * Starts an in-process gateway on 127.0.0.1 with a minimal stdio echo MCP server (written to a temp file, run with the
5
+ * current Node binary) and drives it with N concurrent keep-alive clients for a fixed time per scenario:
6
+ *
7
+ * - `rest`: `POST /api/v1/tools/call` (auth off) — the full pipeline (scopes, policy, quotas, audit in memory);
8
+ * - `rest-auth`: the same with an API key and a rate-limit-free key;
9
+ * - `cache`: a cached tool (`cache.rules`) — the gateway's own overhead without the upstream round trip;
10
+ * - `mcp`: JSON-RPC `tools/call` on the streamable-HTTP `/mcp` endpoint (one session per client).
11
+ *
12
+ * Reports requests/s, mean and p50 / p95 / p99 latency, and errors. Numbers depend on the machine; compare runs on
13
+ * the same host.
14
+ *
15
+ * @module bench
16
+ */
17
+ import { mkdtempSync, writeFileSync, rmSync } from 'fs';
18
+ import { tmpdir } from 'os';
19
+ import { join } from 'path';
20
+ import { Gateway } from '../gateway/index.js';
21
+ export const BENCH_SCENARIOS = ['rest', 'rest-auth', 'cache', 'mcp'];
22
+ const ECHO_SERVER = `
23
+ const rl = require('readline').createInterface({ input: process.stdin });
24
+ const send = (m) => process.stdout.write(JSON.stringify(m) + '\\n');
25
+ rl.on('line', (line) => {
26
+ let msg; try { msg = JSON.parse(line); } catch { return; }
27
+ if (msg.method === 'initialize') return send({ jsonrpc: '2.0', id: msg.id, result: { protocolVersion: '2025-06-18', capabilities: { tools: {} }, serverInfo: { name: 'bench-echo', version: '1' } } });
28
+ if (msg.method === 'tools/list') return send({ jsonrpc: '2.0', id: msg.id, result: { tools: [{ name: 'echo', description: 'Echo', inputSchema: { type: 'object' } }, { name: 'cached', description: 'Cached echo', inputSchema: { type: 'object' } }] } });
29
+ if (msg.method === 'tools/call') return send({ jsonrpc: '2.0', id: msg.id, result: { content: [{ type: 'text', text: JSON.stringify(msg.params.arguments || {}) }] } });
30
+ if (msg.method === 'ping') return send({ jsonrpc: '2.0', id: msg.id, result: {} });
31
+ if (msg.id !== undefined && msg.method) send({ jsonrpc: '2.0', id: msg.id, error: { code: -32601, message: 'unknown' } });
32
+ });
33
+ `;
34
+ export function percentile(sorted, p) {
35
+ if (!sorted.length)
36
+ return 0;
37
+ const i = Math.min(sorted.length - 1, Math.max(0, Math.ceil(p * sorted.length) - 1));
38
+ return sorted[i];
39
+ }
40
+ export function summarize(scenario, latencies, errors, elapsedMs) {
41
+ const s = [...latencies].sort((a, b) => a - b);
42
+ const r2 = (n) => Math.round(n * 100) / 100;
43
+ return {
44
+ scenario,
45
+ requests: s.length,
46
+ errors,
47
+ rps: Math.round((s.length / Math.max(1, elapsedMs)) * 1000),
48
+ meanMs: r2(s.reduce((a, b) => a + b, 0) / Math.max(1, s.length)),
49
+ p50Ms: r2(percentile(s, 0.5)),
50
+ p95Ms: r2(percentile(s, 0.95)),
51
+ p99Ms: r2(percentile(s, 0.99)),
52
+ maxMs: r2(s[s.length - 1] ?? 0),
53
+ };
54
+ }
55
+ async function drive(durationMs, concurrency, once) {
56
+ const latencies = [];
57
+ let errors = 0;
58
+ const t0 = performance.now();
59
+ const end = t0 + durationMs;
60
+ await Promise.all(Array.from({ length: concurrency }, async (_, w) => {
61
+ while (performance.now() < end) {
62
+ const s = performance.now();
63
+ let ok = false;
64
+ try {
65
+ ok = await once(w);
66
+ }
67
+ catch {
68
+ ok = false;
69
+ }
70
+ if (ok)
71
+ latencies.push(performance.now() - s);
72
+ else
73
+ errors++;
74
+ }
75
+ }));
76
+ return { latencies, errors, elapsed: performance.now() - t0 };
77
+ }
78
+ export async function runBenchmark(opts = {}) {
79
+ const durationMs = opts.durationMs ?? 10_000;
80
+ const concurrency = opts.concurrency ?? 32;
81
+ const warmupMs = opts.warmupMs ?? Math.min(1000, durationMs / 5);
82
+ const scenarios = opts.scenarios ?? BENCH_SCENARIOS;
83
+ const dir = mkdtempSync(join(tmpdir(), 'mgw-bench-'));
84
+ const script = join(dir, 'echo.cjs');
85
+ writeFileSync(script, ECHO_SERVER);
86
+ const KEY = 'bench-key-0123456789abcdef';
87
+ const config = {
88
+ port: 0,
89
+ host: '127.0.0.1',
90
+ logLevel: 'error',
91
+ monitor: { requestLog: false },
92
+ servers: [{ id: 'echo', name: 'echo', transport: 'stdio', command: process.execPath, args: [script], maxConcurrency: Math.max(64, concurrency * 2) }],
93
+ cache: { enabled: true, rules: [{ tools: ['cached'], ttlSeconds: 3600, scope: 'shared' }] },
94
+ };
95
+ const results = [];
96
+ const gw = new Gateway(config);
97
+ const gwAuth = new Gateway({ ...config, auth: { strategy: 'api-key', apiKeys: [{ name: 'bench', key: KEY }] } });
98
+ try {
99
+ await gw.start();
100
+ if (scenarios.includes('rest-auth'))
101
+ await gwAuth.start();
102
+ const base = `http://127.0.0.1:${gw.address().port}`;
103
+ const baseAuth = scenarios.includes('rest-auth') ? `http://127.0.0.1:${gwAuth.address().port}` : '';
104
+ const call = (url, tool, headers = {}) => async () => {
105
+ const r = await fetch(`${url}/api/v1/tools/call`, { method: 'POST', headers: { 'content-type': 'application/json', ...headers }, body: JSON.stringify({ server: 'echo', tool, arguments: { n: 1 } }) });
106
+ await r.arrayBuffer();
107
+ return r.ok;
108
+ };
109
+ for (const sc of scenarios) {
110
+ let once;
111
+ if (sc === 'rest')
112
+ once = call(base, 'echo');
113
+ else if (sc === 'rest-auth')
114
+ once = call(baseAuth, 'echo', { authorization: `Bearer ${KEY}` });
115
+ else if (sc === 'cache')
116
+ once = call(base, 'cached');
117
+ else {
118
+ // One MCP session per worker.
119
+ const sessions = [];
120
+ for (let w = 0; w < concurrency; w++) {
121
+ const r = await fetch(`${base}/mcp`, {
122
+ method: 'POST',
123
+ headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream' },
124
+ body: JSON.stringify({ jsonrpc: '2.0', id: 0, method: 'initialize', params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'bench', version: '1' } } }),
125
+ });
126
+ await r.arrayBuffer();
127
+ const sid = r.headers.get('mcp-session-id') ?? '';
128
+ sessions.push(sid);
129
+ await fetch(`${base}/mcp`, { method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream', ...(sid ? { 'mcp-session-id': sid } : {}) }, body: JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) }).then((x) => x.arrayBuffer());
130
+ }
131
+ let id = 1;
132
+ once = async (w) => {
133
+ const sid = sessions[w];
134
+ const r = await fetch(`${base}/mcp`, {
135
+ method: 'POST',
136
+ headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream', ...(sid ? { 'mcp-session-id': sid } : {}) },
137
+ body: JSON.stringify({ jsonrpc: '2.0', id: id++, method: 'tools/call', params: { name: 'echo', arguments: { n: 1 } } }),
138
+ });
139
+ const text = await r.text();
140
+ return r.ok && text.includes('"result"');
141
+ };
142
+ }
143
+ await drive(warmupMs, concurrency, once);
144
+ const { latencies, errors, elapsed } = await drive(durationMs, concurrency, once);
145
+ results.push(summarize(sc, latencies, errors, elapsed));
146
+ }
147
+ }
148
+ finally {
149
+ await gw.stop().catch(() => undefined);
150
+ await gwAuth.stop().catch(() => undefined);
151
+ rmSync(dir, { recursive: true, force: true });
152
+ }
153
+ const os = await import('os');
154
+ return { node: process.version, platform: `${process.platform}-${process.arch}`, cpus: os.cpus().length, concurrency, durationMs, results };
155
+ }
156
+ /** Markdown table of a report. */
157
+ export function benchMarkdown(r) {
158
+ const head = `Node ${r.node} · ${r.platform} · ${r.cpus} CPU · concurrency ${r.concurrency} · ${r.durationMs / 1000}s per scenario\n\n`;
159
+ const rows = r.results.map((x) => `| ${x.scenario} | ${x.rps} | ${x.meanMs} | ${x.p50Ms} | ${x.p95Ms} | ${x.p99Ms} | ${x.errors} |`);
160
+ return head + ['| Scenario | req/s | mean ms | p50 ms | p95 ms | p99 ms | errors |', '|---|---:|---:|---:|---:|---:|---:|', ...rows].join('\n') + '\n';
161
+ }
162
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/bench/index.ts"],"names":[],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -132,9 +132,9 @@ program
132
132
  console.log(` Servers: ${config.servers.length}`);
133
133
  console.log(` Auth: ${config.auth?.strategy ?? 'none'}`);
134
134
  if (config.deprecations?.length) {
135
- console.log('\nDeprecated (removed in 3.0):');
135
+ console.log('\nDeprecated:');
136
136
  for (const d of config.deprecations)
137
- console.log(` ! ${d.message}`);
137
+ console.log(` ! (removed in ${d.removedIn}) ${d.message}${d.detail ? ` — ${d.detail}` : ''}`);
138
138
  }
139
139
  const warnings = securityWarnings(config);
140
140
  if (warnings.length > 0) {
@@ -297,6 +297,81 @@ program
297
297
  console.log(`hash: ${hash}`);
298
298
  console.log('\nGive the key to the client; put the hash in auth.apiKeys. The key is not shown again.');
299
299
  });
300
+ // ─── migrate (3.9) ────────────────────────────────────────────────────────────
301
+ program
302
+ .command('migrate')
303
+ .description('Rewrite a config file to schema v4 (keeps comments); prints the changes')
304
+ .option('-c, --config <path>', 'Config file (default: the usual search paths)')
305
+ .option('--to <version>', 'Target schema version', '4')
306
+ .option('--write', 'Write the file in place (a .bak copy is kept)')
307
+ .option('-o, --output <path>', 'Write the migrated config to this file instead')
308
+ .option('--check', 'Exit with code 3 when the file needs migrating (CI)')
309
+ .action(async (options) => {
310
+ const { readFile } = await import('fs/promises');
311
+ const { migrateConfigText } = await import('./config/migrate.js');
312
+ const path = resolveConfigPath(options.config);
313
+ if (!path) {
314
+ logger.error('No config file found (use -c <path>)');
315
+ process.exit(1);
316
+ }
317
+ try {
318
+ const text = await readFile(path, 'utf8');
319
+ const r = migrateConfigText(text, /\.json$/i.test(path) ? 'json' : 'yaml', Number(options.to));
320
+ if (!r.changed) {
321
+ console.log(`✓ ${path} is already on schema v${options.to}`);
322
+ }
323
+ else {
324
+ console.log(`${path}: ${r.changes.length} change(s)`);
325
+ for (const c of r.changes)
326
+ console.log(` ~ ${c}`);
327
+ }
328
+ for (const n of r.notes)
329
+ console.log(` ! ${n}`);
330
+ if (r.changed && options.check)
331
+ process.exit(3);
332
+ if (!r.changed)
333
+ return;
334
+ if (options.output) {
335
+ await writeFile(options.output, r.text);
336
+ console.log(`Written to ${options.output}`);
337
+ }
338
+ else if (options.write) {
339
+ await writeFile(`${path}.bak`, text);
340
+ await writeFile(path, r.text);
341
+ console.log(`Written (backup: ${path}.bak)`);
342
+ }
343
+ else {
344
+ console.log('\n--- migrated config (use --write to save) ---\n');
345
+ process.stdout.write(r.text);
346
+ }
347
+ }
348
+ catch (err) {
349
+ logger.error(`Migration failed: ${err instanceof Error ? err.message : String(err)}`);
350
+ process.exit(1);
351
+ }
352
+ });
353
+ // ─── bench (3.9) ──────────────────────────────────────────────────────────────
354
+ program
355
+ .command('bench')
356
+ .description('Benchmark an in-process gateway with a local echo server (REST, auth, cache, /mcp)')
357
+ .option('-d, --duration <seconds>', 'Seconds per scenario', '10')
358
+ .option('--concurrency <n>', 'Concurrent clients', '32')
359
+ .option('-s, --scenario <list>', 'Comma-separated: rest,rest-auth,cache,mcp', 'rest,rest-auth,cache,mcp')
360
+ .option('--json', 'Print JSON')
361
+ .action(async (options) => {
362
+ const { runBenchmark, benchMarkdown, BENCH_SCENARIOS } = await import('./bench/index.js');
363
+ const scenarios = String(options.scenario).split(',').map((x) => x.trim()).filter(Boolean);
364
+ const bad = scenarios.filter((x) => !BENCH_SCENARIOS.includes(x));
365
+ const duration = Number(options.duration);
366
+ const concurrency = Number(options.concurrency);
367
+ if (bad.length || !(duration > 0) || !Number.isInteger(concurrency) || concurrency < 1) {
368
+ logger.error(`Invalid options${bad.length ? `: unknown scenario(s) ${bad.join(', ')}` : ''}`);
369
+ process.exit(1);
370
+ }
371
+ logger.setLevel('error');
372
+ const report = await runBenchmark({ durationMs: duration * 1000, concurrency, scenarios: scenarios });
373
+ console.log(options.json ? JSON.stringify(report, null, 2) : benchMarkdown(report));
374
+ });
300
375
  program.parse(process.argv);
301
376
  if (!process.argv.slice(2).length) {
302
377
  program.outputHelp();