@mcpmake/core 0.3.4 → 0.3.6

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 (67) hide show
  1. package/dist/analyzer/goal-crawler.d.ts.map +1 -1
  2. package/dist/analyzer/goal-crawler.js +2 -1
  3. package/dist/analyzer/goal-crawler.js.map +1 -1
  4. package/dist/analyzer/site-crawler.d.ts.map +1 -1
  5. package/dist/analyzer/site-crawler.js +9 -3
  6. package/dist/analyzer/site-crawler.js.map +1 -1
  7. package/dist/emitter/index.d.ts.map +1 -1
  8. package/dist/emitter/index.js +128 -18
  9. package/dist/emitter/index.js.map +1 -1
  10. package/dist/emitter/mcpb-bundler.d.ts +14 -0
  11. package/dist/emitter/mcpb-bundler.d.ts.map +1 -1
  12. package/dist/emitter/mcpb-bundler.js +52 -4
  13. package/dist/emitter/mcpb-bundler.js.map +1 -1
  14. package/dist/emitter/python-annotations.d.ts +77 -0
  15. package/dist/emitter/python-annotations.d.ts.map +1 -0
  16. package/dist/emitter/python-annotations.js +330 -0
  17. package/dist/emitter/python-annotations.js.map +1 -0
  18. package/dist/emitter/python-templates/server.py.hbs +29 -8
  19. package/dist/emitter/site-templates/browser-manager.ts.hbs +140 -30
  20. package/dist/emitter/site-templates/config.ts.hbs +13 -1
  21. package/dist/emitter/site-templates/env.example.hbs +8 -0
  22. package/dist/emitter/site-templates/telemetry.ts.hbs +13 -2
  23. package/dist/emitter/templates/discovery.ts.hbs +43 -4
  24. package/dist/emitter/templates/server-main-http.ts.hbs +79 -1
  25. package/dist/emitter/worker-templates/wrangler.toml.hbs +5 -1
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/parser/har-normalizer.d.ts.map +1 -1
  31. package/dist/parser/har-normalizer.js +27 -2
  32. package/dist/parser/har-normalizer.js.map +1 -1
  33. package/dist/parser/postman-loader.d.ts.map +1 -1
  34. package/dist/parser/postman-loader.js +24 -2
  35. package/dist/parser/postman-loader.js.map +1 -1
  36. package/dist/parser/schema-converter.d.ts +8 -1
  37. package/dist/parser/schema-converter.d.ts.map +1 -1
  38. package/dist/parser/schema-converter.js +20 -2
  39. package/dist/parser/schema-converter.js.map +1 -1
  40. package/dist/recorder/browser-recorder.d.ts.map +1 -1
  41. package/dist/recorder/browser-recorder.js +2 -1
  42. package/dist/recorder/browser-recorder.js.map +1 -1
  43. package/dist/rescan/rescan-scheduler.d.ts +53 -2
  44. package/dist/rescan/rescan-scheduler.d.ts.map +1 -1
  45. package/dist/rescan/rescan-scheduler.js +163 -2
  46. package/dist/rescan/rescan-scheduler.js.map +1 -1
  47. package/dist/transformer/catalog-builder.d.ts +7 -1
  48. package/dist/transformer/catalog-builder.d.ts.map +1 -1
  49. package/dist/transformer/catalog-builder.js +1 -0
  50. package/dist/transformer/catalog-builder.js.map +1 -1
  51. package/dist/transformer/tool-builder.d.ts.map +1 -1
  52. package/dist/transformer/tool-builder.js +4 -1
  53. package/dist/transformer/tool-builder.js.map +1 -1
  54. package/dist/types/index.d.ts +39 -0
  55. package/dist/types/index.d.ts.map +1 -1
  56. package/dist/utils/logger.d.ts +38 -1
  57. package/dist/utils/logger.d.ts.map +1 -1
  58. package/dist/utils/logger.js +75 -1
  59. package/dist/utils/logger.js.map +1 -1
  60. package/dist/utils/playwright-loader.d.ts +21 -0
  61. package/dist/utils/playwright-loader.d.ts.map +1 -0
  62. package/dist/utils/playwright-loader.js +21 -0
  63. package/dist/utils/playwright-loader.js.map +1 -0
  64. package/dist/utils/ssrf-guard.d.ts.map +1 -1
  65. package/dist/utils/ssrf-guard.js +8 -0
  66. package/dist/utils/ssrf-guard.js.map +1 -1
  67. package/package.json +4 -3
@@ -28,6 +28,10 @@ const sessionCreations = new Map<string, Promise<{ page: Page; sessionId: string
28
28
  // Single-flight the browser launch so concurrent cold starts can't spawn
29
29
  // multiple Chromium processes.
30
30
  let browserLaunch: Promise<Browser> | undefined;
31
+ // Incremented whenever closeBrowser() begins. A launch captures the current
32
+ // generation and must close itself instead of becoming active if teardown
33
+ // invalidated it while chromium.launch() was still pending.
34
+ let browserGeneration = 0;
31
35
 
32
36
  const DEFAULT_SESSION = '__default__';
33
37
 
@@ -99,13 +103,35 @@ async function getBrowser(): Promise<Browser> {
99
103
 
100
104
  // Only disable sandbox when running as root in a container.
101
105
  const isRoot = process.getuid?.() === 0;
106
+ const generation = browserGeneration;
102
107
  browserLaunch = chromium
103
108
  .launch({
104
109
  headless: config.headless,
105
110
  args: isRoot ? ['--no-sandbox', '--disable-setuid-sandbox'] : [],
106
111
  })
107
- .then((b) => {
112
+ .then(async (b) => {
113
+ // closeBrowser() may have run while Chromium was launching. Never publish
114
+ // that stale browser into manager state: close it and fail the waiting
115
+ // creation so no resident process escapes the pool/cap.
116
+ if (generation !== browserGeneration) {
117
+ await b.close().catch(() => {});
118
+ throw new Error('Browser launch aborted: browser manager shut down');
119
+ }
108
120
  browser = b;
121
+ // If the whole browser process dies, every session's context is dead.
122
+ // Reclaim them all: emit one session_end each (skipping reserved slots
123
+ // that never started) and clear the pool + idle timer so occupancy frees
124
+ // promptly. No-ops for our own browser.close() (the pool is already empty
125
+ // and `browser` already nulled by then).
126
+ b.on('disconnected', () => {
127
+ if (browser !== b) return;
128
+ browser = undefined;
129
+ clearIdleTimer();
130
+ for (const [id, session] of [...sessions]) {
131
+ sessions.delete(id);
132
+ if (session.context) telemetry.sessionEnded(id, 'crash');
133
+ }
134
+ });
109
135
  return b;
110
136
  })
111
137
  .finally(() => {
@@ -125,20 +151,24 @@ async function getBrowser(): Promise<Browser> {
125
151
  export async function getOrCreateSession(sessionId?: string): Promise<{ page: Page; sessionId: string }> {
126
152
  const id = sessionId ?? DEFAULT_SESSION;
127
153
 
128
- // Reuse existing session
154
+ // Join an in-flight creation for this id FIRST. createSession reserves its slot
155
+ // synchronously (before its first await), so a concurrent second call that
156
+ // checked `sessions` first would observe that placeholder and return its
157
+ // not-yet-assigned (undefined) page. The in-flight promise resolves to the
158
+ // real page, so coalescing here gives every concurrent caller a usable page
159
+ // and a single shared context.
160
+ const inFlight = sessionCreations.get(id);
161
+ if (inFlight) return inFlight;
162
+
163
+ // Reuse a fully-created session (a placeholder reservation has no context yet
164
+ // and is always covered by the in-flight check above).
129
165
  const existing = sessions.get(id);
130
- if (existing) {
166
+ if (existing && existing.context) {
131
167
  existing.lastAccessedAt = Date.now();
132
- resetIdleTimer();
168
+ ensureIdleTimer();
133
169
  return { page: existing.page, sessionId: id };
134
170
  }
135
171
 
136
- // Coalesce concurrent creations for the same id (notably the default
137
- // session) onto one promise so we don't create duplicate contexts or
138
- // double-count capacity.
139
- const inFlight = sessionCreations.get(id);
140
- if (inFlight) return inFlight;
141
-
142
172
  const creation = createSession(id).finally(() => {
143
173
  sessionCreations.delete(id);
144
174
  });
@@ -153,8 +183,8 @@ async function createSession(id: string): Promise<{ page: Page; sessionId: strin
153
183
  const maxSessions = config.maxSessions ?? 10;
154
184
  if (sessions.size >= maxSessions) {
155
185
  throw new Error(
156
- `Session pool at capacity (${maxSessions}). ` +
157
- 'Close an existing session before creating a new one.'
186
+ `Browser session limit reached (${maxSessions}). ` +
187
+ 'Close an existing session and retry.'
158
188
  );
159
189
  }
160
190
  const reserved: BrowserSession = {
@@ -164,28 +194,61 @@ async function createSession(id: string): Promise<{ page: Page; sessionId: strin
164
194
  };
165
195
  sessions.set(id, reserved);
166
196
 
197
+ let context: BrowserContext | undefined;
167
198
  try {
168
199
  const activeBrowser = await getBrowser();
169
- const context = await activeBrowser.newContext({
200
+ context = await activeBrowser.newContext({
170
201
  viewport: config.viewport,
171
202
  ...(config.userAgent ? { userAgent: config.userAgent } : {}),
172
203
  });
173
204
  const page = await context.newPage();
174
205
 
206
+ // If the manager was torn down (shutdown/disconnect) while we awaited the
207
+ // browser/context/page, our reservation was cleared. Do NOT resurrect it:
208
+ // bail so the catch below closes this fresh context. Otherwise we would
209
+ // orphan a live Chromium context outside the pool — escaping the cap, never
210
+ // reaped, and emitting a session_start AFTER its session_end. There is no
211
+ // await between here and `return`, so once this check passes the rest of
212
+ // creation is atomic with respect to teardown.
213
+ if (sessions.get(id) !== reserved) {
214
+ throw new Error('Session creation aborted: browser manager shut down');
215
+ }
216
+
175
217
  reserved.context = context;
176
218
  reserved.page = page;
177
219
  reserved.lastAccessedAt = Date.now();
178
220
 
179
- resetIdleTimer();
221
+ ensureIdleTimer();
180
222
 
181
223
  // The browser context is now resident — start billing this session (no-op
182
224
  // unless the host injected telemetry env).
183
225
  telemetry.sessionStarted(id);
184
226
 
227
+ // Reclaim the slot exactly once if this session dies on its own — a Chromium
228
+ // context close, a renderer (page) crash, or an external close. Every
229
+ // intentional teardown path deletes the map entry BEFORE closing, so the
230
+ // `=== reserved` guard makes this a no-op for those and lets it fire only on
231
+ // an unexpected death, freeing host occupancy promptly instead of waiting
232
+ // out the idle grace. The guard also dedupes a page crash that then closes
233
+ // the context (only the first reclaim wins).
234
+ const reclaim = (reason: string): void => {
235
+ if (sessions.get(id) !== reserved) return;
236
+ sessions.delete(id);
237
+ context?.close().catch(() => {});
238
+ telemetry.sessionEnded(id, reason);
239
+ if (sessions.size === 0) void closeBrowser();
240
+ };
241
+ context.on('close', () => reclaim('crash'));
242
+ page.on('crash', () => reclaim('crash'));
243
+
185
244
  return { page, sessionId: id };
186
245
  } catch (err) {
187
- // Release the reserved slot if context creation failed.
188
- sessions.delete(id);
246
+ // Release the slot AND close any context we created, so a failed or aborted
247
+ // creation never leaks a live Chromium context or pins the cap slot. Only
248
+ // delete the entry if it is still ours (a concurrent shutdown may have
249
+ // already cleared it).
250
+ if (sessions.get(id) === reserved) sessions.delete(id);
251
+ await context?.close().catch(() => {});
189
252
  throw err;
190
253
  }
191
254
  }
@@ -198,10 +261,18 @@ export async function closeSession(sessionId?: string): Promise<void> {
198
261
  const session = sessions.get(id);
199
262
  if (!session) return;
200
263
 
201
- await session.context?.close().catch(() => {});
264
+ // Claim the teardown AND emit session_end SYNCHRONOUSLY, before the first
265
+ // await. Deleting the map entry and removing the telemetry token together,
266
+ // up front, means: (a) a concurrent idle sweep or second close sees the slot
267
+ // gone and skips it; and (b) a same-id session re-created during the
268
+ // context-close await below gets its OWN fresh telemetry token instead of
269
+ // sharing this one (which would suppress its later session_end). Exactly one
270
+ // session_end per teardown.
202
271
  sessions.delete(id);
203
272
  telemetry.sessionEnded(id, 'explicit');
204
273
 
274
+ await session.context?.close().catch(() => {});
275
+
205
276
  // If no sessions remain, close the browser
206
277
  if (sessions.size === 0) {
207
278
  await closeBrowser();
@@ -213,17 +284,42 @@ export async function closeSession(sessionId?: string): Promise<void> {
213
284
  */
214
285
  export async function closeBrowser(): Promise<void> {
215
286
  clearIdleTimer();
216
-
217
- for (const [id, session] of sessions) {
287
+ // Invalidate a chromium.launch() already in progress. Its completion handler
288
+ // will close the resulting process rather than assigning it to `browser`.
289
+ browserGeneration++;
290
+ const launching = browserLaunch;
291
+
292
+ // Null `browser` synchronously BEFORE awaiting its close. While close() is in
293
+ // flight the instance still reports isConnected() === true, so without this a
294
+ // concurrent getOrCreateSession would be handed the dying browser and build a
295
+ // context on it that dies immediately. Nulling up front forces a relaunch
296
+ // instead, and single-flights closeBrowser (a second concurrent call sees no
297
+ // browser and skips the redundant close()).
298
+ const dying = browser;
299
+ browser = undefined;
300
+
301
+ // Snapshot and clear the pool, then emit every session_end SYNCHRONOUSLY
302
+ // (before any await) so a same-id session re-created during the context-close
303
+ // awaits gets its own telemetry token rather than sharing a snapshotted one.
304
+ // Reserved-but-unstarted slots are skipped: no context to close, and no
305
+ // session_start to balance (the in-flight createSession aborts itself once it
306
+ // sees its reservation cleared).
307
+ const entries = [...sessions];
308
+ sessions.clear();
309
+ for (const [id, session] of entries) {
310
+ if (session.context) telemetry.sessionEnded(id, 'shutdown');
311
+ }
312
+ for (const [, session] of entries) {
218
313
  await session.context?.close().catch(() => {});
219
- sessions.delete(id);
220
- telemetry.sessionEnded(id, 'shutdown');
221
314
  }
222
315
 
223
- if (browser) {
224
- await browser.close().catch(() => {});
225
- browser = undefined;
316
+ if (dying) {
317
+ await dying.close().catch(() => {});
226
318
  }
319
+ // Wait for an invalidated launch to finish its self-cleanup. This makes
320
+ // closeBrowser() a real lifecycle barrier: after it resolves, no Chromium
321
+ // process from work that began before teardown can still become resident.
322
+ await launching?.catch(() => {});
227
323
  }
228
324
 
229
325
  /**
@@ -235,7 +331,9 @@ export async function takeScreenshot(
235
331
  ): Promise<Buffer> {
236
332
  const id = sessionId ?? DEFAULT_SESSION;
237
333
  const session = sessions.get(id);
238
- if (!session) {
334
+ // A reserved, mid-creation slot is in the map but has no page yet — treat it
335
+ // as not-yet-active so callers get the clear error, not a TypeError.
336
+ if (!session || !session.context) {
239
337
  throw new Error(`No active session: ${id}`);
240
338
  }
241
339
  session.lastAccessedAt = Date.now();
@@ -243,10 +341,13 @@ export async function takeScreenshot(
243
341
  }
244
342
 
245
343
  /**
246
- * List active session IDs.
344
+ * List active session IDs. Excludes reserved, mid-creation slots (no page yet)
345
+ * so a returned id is always usable with takeScreenshot/tool calls.
247
346
  */
248
347
  export function listSessions(): string[] {
249
- return Array.from(sessions.keys());
348
+ return Array.from(sessions.entries())
349
+ .filter(([, session]) => session.context !== undefined)
350
+ .map(([id]) => id);
250
351
  }
251
352
 
252
353
  /**
@@ -258,16 +359,25 @@ export function isBrowserRunning(): boolean {
258
359
 
259
360
  // ─── Idle Timer ─────────────────────────────────────────────────────
260
361
 
261
- function resetIdleTimer(): void {
262
- clearIdleTimer();
362
+ // Start the idle reaper once and let it tick at a FIXED cadence. It must NOT be
363
+ // reset on session access: resetting on every access would let a busy container
364
+ // (one session touched within each interval) starve the reaper so it never
365
+ // fires, leaving genuinely-idle sibling sessions resident forever. Eviction
366
+ // keys off each session's own lastAccessedAt, so a steady tick is sufficient —
367
+ // touching a session keeps only that session alive.
368
+ function ensureIdleTimer(): void {
369
+ if (idleTimer) return;
263
370
  idleTimer = setInterval(() => {
264
371
  const now = Date.now();
265
372
  // Close idle sessions. Skip slots still being created (no context yet).
266
373
  for (const [id, session] of sessions) {
267
374
  if (!session.context) continue;
268
375
  if (now - session.lastAccessedAt > config.idleTimeoutMs) {
269
- session.context.close().catch(() => {});
376
+ // Delete before closing so the context's own 'close' handler (and any
377
+ // concurrent teardown) sees the slot already claimed — session_end is
378
+ // emitted once, here, with reason 'idle'.
270
379
  sessions.delete(id);
380
+ session.context.close().catch(() => {});
271
381
  telemetry.sessionEnded(id, 'idle');
272
382
  }
273
383
  }
@@ -16,6 +16,15 @@ const posInt = (v: string | undefined, d: number): number => {
16
16
  return Number.isInteger(n) && n > 0 ? n : d;
17
17
  };
18
18
 
19
+ // Hard ceiling on concurrently-resident browser sessions for THIS container.
20
+ // The cloud host injects MCP_MAX_BROWSER_SESSIONS at container start to pin the
21
+ // owner's per-plan concurrent-browser limit, making the container's hard cap the
22
+ // plan cap. MAX_SESSIONS is the legacy/self-host alias. Either env var, when a
23
+ // valid positive integer, overrides the baked-in default; an unset or invalid
24
+ // value falls through to the next source. DEFAULT_MAX_SESSIONS is the safe
25
+ // fallback. The browser manager refuses to launch a session past this cap.
26
+ const DEFAULT_MAX_SESSIONS = {{#if browserConfig.maxSessions}}{{browserConfig.maxSessions}}{{else}}10{{/if}};
27
+
19
28
  export function loadConfig(): AppConfig {
20
29
  return {
21
30
  baseUrl: process.env.BASE_URL ?? '{{{baseUrl}}}',
@@ -27,7 +36,10 @@ export function loadConfig(): AppConfig {
27
36
  height: posInt(process.env.VIEWPORT_HEIGHT, {{browserConfig.viewport.height}}),
28
37
  },
29
38
  userAgent: process.env.USER_AGENT || undefined,
30
- maxSessions: posInt(process.env.MAX_SESSIONS, {{browserConfig.maxSessions}}),
39
+ maxSessions: posInt(
40
+ process.env.MCP_MAX_BROWSER_SESSIONS,
41
+ posInt(process.env.MAX_SESSIONS, DEFAULT_MAX_SESSIONS),
42
+ ),
31
43
  },
32
44
  };
33
45
  }
@@ -10,6 +10,14 @@ VIEWPORT_WIDTH={{browserConfig.viewport.width}}
10
10
  VIEWPORT_HEIGHT={{browserConfig.viewport.height}}
11
11
  # USER_AGENT=
12
12
 
13
+ # Hard cap on concurrently-resident browser sessions (Chromium contexts) this
14
+ # server will hold. A tool call that would open one past the cap is refused with
15
+ # a clear error rather than launching another browser. Managed hosting injects
16
+ # this to enforce the plan's concurrent-browser limit. MAX_SESSIONS is a legacy
17
+ # alias; MCP_MAX_BROWSER_SESSIONS wins when both are set.
18
+ MCP_MAX_BROWSER_SESSIONS={{#if browserConfig.maxSessions}}{{browserConfig.maxSessions}}{{else}}10{{/if}}
19
+ # MAX_SESSIONS={{#if browserConfig.maxSessions}}{{browserConfig.maxSessions}}{{else}}10{{/if}}
20
+
13
21
  # Browser-session telemetry (managed hosting only — leave UNSET to self-host).
14
22
  # When the host sets these, the server reports exact browser-session lifetime
15
23
  # for metering. It NEVER phones home unless MCPMAKE_TELEMETRY_URL is set.
@@ -66,10 +66,16 @@ export function sessionStarted(sessionId: string): void {
66
66
  ensureTimer();
67
67
  }
68
68
 
69
- /** Record that a browser session was torn down (idle | explicit | shutdown). */
69
+ /** Record that a browser session was torn down (idle | explicit | shutdown |
70
+ * crash). Idempotent: emits at most one session_end per session. A second end
71
+ * for the same id (e.g. an explicit close that resumes after flushOnShutdown
72
+ * already ended it on SIGTERM, or a crash racing a teardown) is a no-op, and an
73
+ * end for a session that never started (a reserved-but-unstarted slot) emits
74
+ * nothing — so the host's occupancy ledger can never be double-decremented. */
70
75
  export function sessionEnded(sessionId: string, reason: string): void {
71
76
  if (!ENABLED) return;
72
- openSessions.delete(sessionId);
77
+ // openSessions is the source of truth: only the call that removes the id emits.
78
+ if (!openSessions.delete(sessionId)) return;
73
79
  pushEvent({ type: 'session_end', sessionId, ts: Date.now(), reason });
74
80
  // Flush promptly so the host can finalize this session's billing.
75
81
  void flush();
@@ -108,6 +114,11 @@ async function flush(): Promise<void> {
108
114
  /** Best-effort final flush on shutdown: end every open session and send. */
109
115
  export async function flushOnShutdown(reason = 'shutdown'): Promise<void> {
110
116
  if (!ENABLED) return;
117
+ // Stop the heartbeat interval so no flush can fire after this final boundary.
118
+ if (flushTimer) {
119
+ clearInterval(flushTimer);
120
+ flushTimer = undefined;
121
+ }
111
122
  const now = Date.now();
112
123
  for (const id of [...openSessions]) {
113
124
  openSessions.delete(id);
@@ -10,6 +10,7 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
10
  import { executeRequest } from './http.js';
11
11
  import { applyJqFilter } from './response-filter.js';
12
12
  import type { AppConfig } from './config.js';
13
+ import type { AuthRequirement } from './auth.js';
13
14
  import catalogData from './tool-catalog.json' with { type: 'json' };
14
15
 
15
16
  interface CatalogParamMapping {
@@ -35,6 +36,10 @@ interface CatalogEntry {
35
36
  /** The MCP input key for the request body (`body`, `requestBody`, etc.).
36
37
  * Only present when `hasRequestBody` is true. */
37
38
  bodyInputKey?: string;
39
+ /** Per-operation outbound-auth requirement (D-H2). Forwarded to executeRequest
40
+ * so a public operation (`security: []`) sends no auth and a scheme-scoped
41
+ * operation sends only its scheme(s). Absent → every configured scheme. */
42
+ authRequirement?: AuthRequirement;
38
43
  }
39
44
 
40
45
  const catalog: CatalogEntry[] = catalogData as CatalogEntry[];
@@ -169,15 +174,46 @@ export function registerDiscoveryTools(server: McpServer, config: AppConfig): vo
169
174
  }
170
175
 
171
176
  try {
172
- // Build URL from path template — read args by inputKey, substitute wireName
173
- // into the URL template. Handles parameter name collisions (R14-A).
174
- let url = config.baseUrl + tool.path;
177
+ // Build the path suffix from the template — read args by inputKey,
178
+ // substitute wireName into the path. Handles parameter name collisions
179
+ // (R14-A). The leftover-placeholder check (R5-F) runs on the PATH ONLY,
180
+ // not on config.baseUrl: a base URL may legitimately contain an
181
+ // unresolved OpenAPI server variable (e.g. `https://api.{region}.x.com`)
182
+ // that is not a tool path parameter, and scanning it would falsely fail
183
+ // every call.
184
+ let pathSuffix = tool.path;
175
185
  for (const p of tool.pathParams) {
176
186
  if (args[p.inputKey] !== undefined) {
177
- url = url.replace(`{${p.wireName}}`, encodeURIComponent(String(args[p.inputKey])));
187
+ pathSuffix = pathSuffix.replace(
188
+ `{${p.wireName}}`,
189
+ encodeURIComponent(String(args[p.inputKey])),
190
+ );
178
191
  }
179
192
  }
180
193
 
194
+ // R5-F: refuse to fire a request with unsubstituted path placeholders.
195
+ // A missing required path parameter leaves a literal `{name}` in the
196
+ // path, which would otherwise be sent verbatim upstream. Return an MCP
197
+ // error listing the unfilled parameter(s) instead.
198
+ const missing = pathSuffix.match(/\{[^}]+\}/g);
199
+ if (missing && missing.length > 0) {
200
+ return {
201
+ content: [
202
+ {
203
+ type: 'text' as const,
204
+ text: `Missing required path parameter(s): ${missing
205
+ .map((s) => s.slice(1, -1))
206
+ .join(', ')}. Use get_tool_schema to see required inputs.`,
207
+ },
208
+ ],
209
+ isError: true,
210
+ };
211
+ }
212
+
213
+ // Path is fully substituted — prepend the (possibly server-variable-
214
+ // bearing) base URL to form the request URL.
215
+ let url = config.baseUrl + pathSuffix;
216
+
181
217
  // Add query params — read args by inputKey, send upstream under wireName (R14-A).
182
218
  const queryParams = new URLSearchParams();
183
219
  for (const p of tool.queryParams) {
@@ -198,6 +234,9 @@ export function registerDiscoveryTools(server: McpServer, config: AppConfig): vo
198
234
  headers: {},
199
235
  config,
200
236
  idempotencyKey,
237
+ // R5-E: scope outbound auth per operation exactly like the static
238
+ // per-tool handlers. Absent → executeRequest applies every scheme.
239
+ authRequirement: tool.authRequirement,
201
240
  });
202
241
 
203
242
  const result =
@@ -174,6 +174,39 @@ if (transportMode === 'http') {
174
174
  }
175
175
  const allowedOrigin = process.env.ALLOWED_ORIGIN;
176
176
  const authToken = process.env.MCP_AUTH_TOKEN;
177
+ {{#if hasOAuth}}
178
+ /* ── OAuth issuer resolution inputs ──────────────────────────────────────
179
+ * The unauthenticated /.well-known/oauth-authorization-server endpoint must
180
+ * not reflect an arbitrary client-controlled Host header into the issuer
181
+ * (host-header spoofing). Operators pin the issuer with one of:
182
+ * - ALLOWED_ORIGIN (existing; takes precedence)
183
+ * - MCP_PUBLIC_URL (absolute http(s) URL; its origin is used)
184
+ * - MCP_ALLOWED_HOSTS (comma/space-separated Host allowlist)
185
+ * With none set we fall back to the request Host for backward-compat (dev),
186
+ * after warning once that the issuer is Host-derived. */
187
+ const mcpPublicOrigin = (() => {
188
+ const raw = process.env.MCP_PUBLIC_URL;
189
+ if (!raw) return undefined;
190
+ try {
191
+ const u = new URL(raw);
192
+ if (u.protocol !== 'http:' && u.protocol !== 'https:') {
193
+ log('error', 'MCP_PUBLIC_URL must be an http(s) URL — ignoring', { MCP_PUBLIC_URL: raw });
194
+ return undefined;
195
+ }
196
+ return u.origin;
197
+ } catch {
198
+ log('error', 'MCP_PUBLIC_URL is not a valid absolute URL — ignoring', { MCP_PUBLIC_URL: raw });
199
+ return undefined;
200
+ }
201
+ })();
202
+ const allowedHosts = new Set(
203
+ (process.env.MCP_ALLOWED_HOSTS ?? '')
204
+ .split(/[\s,]+/)
205
+ .map((h) => h.trim().toLowerCase())
206
+ .filter((h) => h.length > 0),
207
+ );
208
+ let warnedHostDerivedIssuer = false;
209
+ {{/if}}
177
210
  /* Fail closed: with no MCP_AUTH_TOKEN the server denies every authenticated
178
211
  * route (401) unless the operator opted in via MCP_ALLOW_UNAUTHENTICATED=true.
179
212
  * Warn loudly, once, when running wide open so it is never a silent default. */
@@ -369,7 +402,52 @@ if (transportMode === 'http') {
369
402
  return;
370
403
  }
371
404
  const proto = req.headers['x-forwarded-proto'] === 'https' ? 'https' : 'http';
372
- const issuer = allowedOrigin ?? `${proto}://${req.headers.host ?? 'localhost'}`;
405
+ /* Resolve the OAuth issuer without trusting an arbitrary Host header.
406
+ * Precedence: ALLOWED_ORIGIN → MCP_PUBLIC_URL → MCP_ALLOWED_HOSTS →
407
+ * (dev fallback) the request Host, but only when no allowlist/public
408
+ * URL pins it. When MCP_ALLOWED_HOSTS is configured, a Host outside it
409
+ * is rejected rather than reflected. */
410
+ const rawHost = req.headers.host;
411
+ const host = typeof rawHost === 'string' ? rawHost.toLowerCase() : undefined;
412
+ // Parse the Host once. `hostname` (no port, no userinfo) drives the
413
+ // allowlist so a bare-hostname allowlist (MCP_ALLOWED_HOSTS=example.com)
414
+ // matches `Host: example.com:3000`; `hostPort` (hostname:port, userinfo
415
+ // STRIPPED) builds the issuer so a `Host: evil@example.com` cannot inject
416
+ // a userinfo segment into the issued metadata. URL parsing also handles
417
+ // IPv6 brackets. Falls back to undefined when the Host is malformed.
418
+ const parsedHost = (() => {
419
+ if (typeof rawHost !== 'string') return undefined;
420
+ try {
421
+ const u = new URL(`http://${rawHost}`);
422
+ return { hostname: u.hostname.toLowerCase(), hostPort: u.host };
423
+ } catch {
424
+ return undefined;
425
+ }
426
+ })();
427
+ const hostNoPort = parsedHost?.hostname ?? host;
428
+ let issuer: string;
429
+ if (allowedOrigin) {
430
+ issuer = allowedOrigin;
431
+ } else if (mcpPublicOrigin) {
432
+ issuer = mcpPublicOrigin;
433
+ } else if (allowedHosts.size > 0) {
434
+ if (!host || (!allowedHosts.has(host) && !(hostNoPort && allowedHosts.has(hostNoPort)))) {
435
+ log('error', 'Invalid Host header for OAuth metadata', { host: rawHost });
436
+ sendJson(res, 400, { error: 'Invalid Host header for OAuth metadata' });
437
+ return;
438
+ }
439
+ // Build from the parsed host:port (userinfo stripped), never raw.
440
+ issuer = `${proto}://${parsedHost?.hostPort ?? rawHost}`;
441
+ } else {
442
+ /* No issuer pinned: reflect the request Host for backward-compat with
443
+ * proxy deployments that set neither var. Warn once so operators know
444
+ * to lock it down via MCP_PUBLIC_URL or MCP_ALLOWED_HOSTS. */
445
+ if (!warnedHostDerivedIssuer) {
446
+ warnedHostDerivedIssuer = true;
447
+ log('info', 'OAuth issuer derived from the Host header — set MCP_PUBLIC_URL or MCP_ALLOWED_HOSTS to pin it', { host: rawHost });
448
+ }
449
+ issuer = `${proto}://${rawHost ?? 'localhost'}`;
450
+ }
373
451
  sendJson(
374
452
  res,
375
453
  200,
@@ -7,7 +7,11 @@ compatibility_flags = ["nodejs_compat"]
7
7
 
8
8
  # Non-secret configuration. Override per-environment as needed.
9
9
  [vars]
10
- BASE_URL = "{{baseUrl}}"
10
+ # {{{json}}} emits a JSON string literal, which is a valid TOML basic string with
11
+ # identical escape semantics. Unlike a raw "{{baseUrl}}" interpolation, this fully
12
+ # escapes backslashes/quotes/newlines so an attacker-controlled URL cannot inject a
13
+ # spurious key or [section] header into the generated wrangler.toml (A4-15).
14
+ BASE_URL = {{{json baseUrl}}}
11
15
  MAX_RETRIES = "3"
12
16
  REQUEST_INTERVAL_MS = "100"
13
17
  # ALLOWED_ORIGIN = "https://your-client.example.com"
package/dist/index.d.ts CHANGED
@@ -74,6 +74,7 @@ export { fail } from './utils/fail.js';
74
74
  export { pathExists } from './utils/fs.js';
75
75
  export { confirmOperations } from './utils/interactive.js';
76
76
  export { watchFile } from './utils/watcher.js';
77
+ export { loadChromium } from './utils/playwright-loader.js';
77
78
  export { FAMILY_A_PRICING, DEFAULT_PRICING_SERVER, fetchPricing, formatPrice } from './pricing.js';
78
79
  export type { BillingPeriod, PricePoint } from './pricing.js';
79
80
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -97,6 +97,7 @@ export { fail } from './utils/fail.js';
97
97
  export { pathExists } from './utils/fs.js';
98
98
  export { confirmOperations } from './utils/interactive.js';
99
99
  export { watchFile } from './utils/watcher.js';
100
+ export { loadChromium } from './utils/playwright-loader.js';
100
101
  // ---------------------------------------------------------------------------
101
102
  // Pricing
102
103
  // ---------------------------------------------------------------------------
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,8EAA8E;AAC9E,4BAA4B;AAC5B,8EAA8E;AAC9E,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,GAChB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AAExE,8EAA8E;AAC9E,6CAA6C;AAC7C,8EAA8E;AAC9E,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAE7D,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACpG,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AAEpE,OAAO,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,MAAM,8BAA8B,CAAC;AAC9F,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE1D,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAE5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAEnE,8EAA8E;AAC9E,sDAAsD;AACtD,8EAA8E;AAC9E,OAAO,EACL,UAAU,EACV,WAAW,EACX,UAAU,EACV,cAAc,EACd,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACnF,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AACrE,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACjF,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAEnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAEhE,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAE1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAC1F,OAAO,EAAE,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AAE3E,8EAA8E;AAC9E,+CAA+C;AAC/C,8EAA8E;AAC9E,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAEvD,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AACpF,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAE9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAG/D,8EAA8E;AAC9E,sDAAsD;AACtD,8EAA8E;AAC9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AACzE,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AACjF,OAAO,EAAE,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AAE3E,8EAA8E;AAC9E,iCAAiC;AACjC,8EAA8E;AAC9E,OAAO,EAAE,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AAE5E,8EAA8E;AAC9E,gCAAgC;AAChC,8EAA8E;AAC9E,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAGpF,8EAA8E;AAC9E,qCAAqC;AACrC,8EAA8E;AAC9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AAEtE,8EAA8E;AAC9E,mCAAmC;AACnC,8EAA8E;AAC9E,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAE/E,OAAO,EAAE,6BAA6B,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAG3F,8EAA8E;AAC9E,0CAA0C;AAC1C,8EAA8E;AAC9E,OAAO,EAAE,yBAAyB,EAAE,MAAM,kCAAkC,CAAC;AAE7E,8EAA8E;AAC9E,6BAA6B;AAC7B,8EAA8E;AAC9E,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGhF,8EAA8E;AAC9E,QAAQ;AACR,8EAA8E;AAC9E,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAEhC,8EAA8E;AAC9E,QAAQ;AACR,8EAA8E;AAC9E,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE/C,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAC9E,OAAO,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"har-normalizer.d.ts","sourceRoot":"","sources":["../../src/parser/har-normalizer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAOxC,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,KAAK,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,WAAW,EAAE,UAAU,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAC;CAC7C;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;CAChD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,GAAG,eAAe,CAgD5D"}
1
+ {"version":3,"file":"har-normalizer.d.ts","sourceRoot":"","sources":["../../src/parser/har-normalizer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAOxC,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,KAAK,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,WAAW,EAAE,UAAU,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAC;CAC7C;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;CAChD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,GAAG,eAAe,CAwD5D"}
@@ -39,6 +39,12 @@ export function normalizeEntry(entry) {
39
39
  const normalizedPath = '/' + normalizedSegments.join('/');
40
40
  const queryParams = [];
41
41
  for (const qs of entry.request.queryString ?? []) {
42
+ if (qs === null ||
43
+ typeof qs !== 'object' ||
44
+ typeof qs.name !== 'string' ||
45
+ typeof qs.value !== 'string') {
46
+ continue;
47
+ }
42
48
  queryParams.push({
43
49
  name: qs.name,
44
50
  exampleValue: qs.value,
@@ -56,9 +62,28 @@ function deriveParamName(segments, currentIndex, counter) {
56
62
  }
57
63
  return counter === 0 ? 'id' : `id${counter + 1}`;
58
64
  }
65
+ // Version / routing prefixes that look plural-ish or otherwise sneak past the
66
+ // pattern but are never REST collections. `vN` is matched structurally below.
67
+ const NON_COLLECTION_SEGMENTS = new Set(['api', 'rest', 'graphql', 'rpc', 'status']);
59
68
  function isCollectionName(segment) {
60
- // Common REST patterns: plurals, known resource names
61
- return (/^[a-z][a-z0-9_-]*s$/i.test(segment) || ['api', 'v1', 'v2', 'v3'].includes(segment) === false);
69
+ // Discriminating REST collection heuristic. The previous version returned true
70
+ // for almost any segment (its second clause was `!['api','v1',...].includes(x)`,
71
+ // i.e. true for everything except those four), which over-parameterized paths.
72
+ //
73
+ // A collection segment is a *plural resource name*: lowercase, alphanumeric
74
+ // (allowing `_`/`-`), and ending in `s` but not `ss` (e.g. `users`, `posts`,
75
+ // `items`, `api_keys` — but not `class`, `address`). We additionally exclude
76
+ // version prefixes (`v1`, `v2`, …) and a small set of routing/non-resource
77
+ // tokens, so a generic singular segment like `user`, `profile`, or a version
78
+ // like `v1` is no longer treated as a collection.
79
+ const s = segment.toLowerCase();
80
+ if (NON_COLLECTION_SEGMENTS.has(s))
81
+ return false;
82
+ if (/^v\d+$/.test(s))
83
+ return false; // version prefixes: v1, v2, v10, ...
84
+ // Plural resource: starts with a letter, ends in `s`, but the char before the
85
+ // trailing `s` is not itself `s` (excludes `class`, `address`, `status`).
86
+ return /^[a-z][a-z0-9_-]*[^s]s$/.test(s);
62
87
  }
63
88
  function singularize(word) {
64
89
  if (word.endsWith('ies'))