@mcpmake/core 0.3.3 → 0.3.5

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 (130) hide show
  1. package/dist/analyzer/goal-crawler.d.ts.map +1 -1
  2. package/dist/analyzer/goal-crawler.js +6 -12
  3. package/dist/analyzer/goal-crawler.js.map +1 -1
  4. package/dist/analyzer/same-origin.d.ts +21 -0
  5. package/dist/analyzer/same-origin.d.ts.map +1 -0
  6. package/dist/analyzer/same-origin.js +32 -0
  7. package/dist/analyzer/same-origin.js.map +1 -0
  8. package/dist/analyzer/semantic-analyzer.d.ts.map +1 -1
  9. package/dist/analyzer/semantic-analyzer.js +3 -0
  10. package/dist/analyzer/semantic-analyzer.js.map +1 -1
  11. package/dist/analyzer/site-crawler.d.ts.map +1 -1
  12. package/dist/analyzer/site-crawler.js +12 -13
  13. package/dist/analyzer/site-crawler.js.map +1 -1
  14. package/dist/emitter/index.d.ts.map +1 -1
  15. package/dist/emitter/index.js +149 -19
  16. package/dist/emitter/index.js.map +1 -1
  17. package/dist/emitter/mcpb-bundler.d.ts +14 -0
  18. package/dist/emitter/mcpb-bundler.d.ts.map +1 -1
  19. package/dist/emitter/mcpb-bundler.js +52 -4
  20. package/dist/emitter/mcpb-bundler.js.map +1 -1
  21. package/dist/emitter/project-scaffolder.d.ts +5 -0
  22. package/dist/emitter/project-scaffolder.d.ts.map +1 -1
  23. package/dist/emitter/project-scaffolder.js +1 -1
  24. package/dist/emitter/project-scaffolder.js.map +1 -1
  25. package/dist/emitter/python-annotations.d.ts +77 -0
  26. package/dist/emitter/python-annotations.d.ts.map +1 -0
  27. package/dist/emitter/python-annotations.js +330 -0
  28. package/dist/emitter/python-annotations.js.map +1 -0
  29. package/dist/emitter/python-template-loader.js +1 -1
  30. package/dist/emitter/python-template-loader.js.map +1 -1
  31. package/dist/emitter/python-templates/server.py.hbs +69 -33
  32. package/dist/emitter/site-templates/browser-manager.ts.hbs +145 -32
  33. package/dist/emitter/site-templates/config.ts.hbs +21 -4
  34. package/dist/emitter/site-templates/env.example.hbs +8 -0
  35. package/dist/emitter/site-templates/server-main-http.ts.hbs +13 -4
  36. package/dist/emitter/site-templates/server-main.ts.hbs +5 -2
  37. package/dist/emitter/site-templates/telemetry.ts.hbs +13 -2
  38. package/dist/emitter/templates/config.ts.hbs +29 -2
  39. package/dist/emitter/templates/discovery.ts.hbs +66 -13
  40. package/dist/emitter/templates/http-executor.ts.hbs +4 -3
  41. package/dist/emitter/templates/oauth.ts.hbs +1 -2
  42. package/dist/emitter/templates/readme.md.hbs +3 -2
  43. package/dist/emitter/templates/resources.ts.hbs +3 -2
  44. package/dist/emitter/templates/server-main-http.ts.hbs +218 -130
  45. package/dist/emitter/templates/task-handlers.ts.hbs +2 -1
  46. package/dist/emitter/templates/task-manager.ts.hbs +5 -3
  47. package/dist/emitter/templates/task-sse.ts.hbs +9 -1
  48. package/dist/emitter/templates/tool-handler.ts.hbs +17 -4
  49. package/dist/emitter/worker-templates/config.ts.hbs +25 -2
  50. package/dist/emitter/worker-templates/worker.ts.hbs +6 -2
  51. package/dist/emitter/worker-templates/wrangler.toml.hbs +5 -1
  52. package/dist/index.d.ts +1 -0
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +1 -0
  55. package/dist/index.js.map +1 -1
  56. package/dist/parser/har-normalizer.d.ts.map +1 -1
  57. package/dist/parser/har-normalizer.js +28 -3
  58. package/dist/parser/har-normalizer.js.map +1 -1
  59. package/dist/parser/openapi-loader.d.ts +19 -0
  60. package/dist/parser/openapi-loader.d.ts.map +1 -1
  61. package/dist/parser/openapi-loader.js +78 -10
  62. package/dist/parser/openapi-loader.js.map +1 -1
  63. package/dist/parser/operation-extractor.js +4 -4
  64. package/dist/parser/operation-extractor.js.map +1 -1
  65. package/dist/parser/overlay-loader.js +50 -11
  66. package/dist/parser/overlay-loader.js.map +1 -1
  67. package/dist/parser/postman-loader.d.ts.map +1 -1
  68. package/dist/parser/postman-loader.js +64 -6
  69. package/dist/parser/postman-loader.js.map +1 -1
  70. package/dist/parser/schema-converter.d.ts +15 -1
  71. package/dist/parser/schema-converter.d.ts.map +1 -1
  72. package/dist/parser/schema-converter.js +30 -2
  73. package/dist/parser/schema-converter.js.map +1 -1
  74. package/dist/recorder/browser-recorder.d.ts.map +1 -1
  75. package/dist/recorder/browser-recorder.js +2 -1
  76. package/dist/recorder/browser-recorder.js.map +1 -1
  77. package/dist/rescan/diff-engine.js +17 -10
  78. package/dist/rescan/diff-engine.js.map +1 -1
  79. package/dist/rescan/rescan-runner.js +4 -4
  80. package/dist/rescan/rescan-runner.js.map +1 -1
  81. package/dist/rescan/rescan-scheduler.d.ts +54 -2
  82. package/dist/rescan/rescan-scheduler.d.ts.map +1 -1
  83. package/dist/rescan/rescan-scheduler.js +193 -19
  84. package/dist/rescan/rescan-scheduler.js.map +1 -1
  85. package/dist/site-transformer/selector-healer.d.ts.map +1 -1
  86. package/dist/site-transformer/selector-healer.js +9 -0
  87. package/dist/site-transformer/selector-healer.js.map +1 -1
  88. package/dist/site-transformer/tool-generator.js +4 -7
  89. package/dist/site-transformer/tool-generator.js.map +1 -1
  90. package/dist/transformer/auth-detector.d.ts.map +1 -1
  91. package/dist/transformer/auth-detector.js +49 -1
  92. package/dist/transformer/auth-detector.js.map +1 -1
  93. package/dist/transformer/catalog-builder.d.ts +25 -3
  94. package/dist/transformer/catalog-builder.d.ts.map +1 -1
  95. package/dist/transformer/catalog-builder.js +32 -12
  96. package/dist/transformer/catalog-builder.js.map +1 -1
  97. package/dist/transformer/client-compat.d.ts.map +1 -1
  98. package/dist/transformer/client-compat.js +17 -6
  99. package/dist/transformer/client-compat.js.map +1 -1
  100. package/dist/transformer/har-dedup.js +2 -2
  101. package/dist/transformer/har-dedup.js.map +1 -1
  102. package/dist/transformer/har-to-operations.d.ts.map +1 -1
  103. package/dist/transformer/har-to-operations.js +10 -3
  104. package/dist/transformer/har-to-operations.js.map +1 -1
  105. package/dist/transformer/llm-namer.d.ts.map +1 -1
  106. package/dist/transformer/llm-namer.js +3 -0
  107. package/dist/transformer/llm-namer.js.map +1 -1
  108. package/dist/transformer/resource-builder.d.ts.map +1 -1
  109. package/dist/transformer/resource-builder.js +112 -18
  110. package/dist/transformer/resource-builder.js.map +1 -1
  111. package/dist/transformer/stainless-translator.d.ts.map +1 -1
  112. package/dist/transformer/stainless-translator.js +5 -2
  113. package/dist/transformer/stainless-translator.js.map +1 -1
  114. package/dist/transformer/tool-builder.d.ts.map +1 -1
  115. package/dist/transformer/tool-builder.js +67 -16
  116. package/dist/transformer/tool-builder.js.map +1 -1
  117. package/dist/types/index.d.ts +39 -0
  118. package/dist/types/index.d.ts.map +1 -1
  119. package/dist/utils/logger.d.ts +38 -1
  120. package/dist/utils/logger.d.ts.map +1 -1
  121. package/dist/utils/logger.js +75 -1
  122. package/dist/utils/logger.js.map +1 -1
  123. package/dist/utils/playwright-loader.d.ts +21 -0
  124. package/dist/utils/playwright-loader.d.ts.map +1 -0
  125. package/dist/utils/playwright-loader.js +21 -0
  126. package/dist/utils/playwright-loader.js.map +1 -0
  127. package/dist/utils/ssrf-guard.d.ts.map +1 -1
  128. package/dist/utils/ssrf-guard.js +8 -0
  129. package/dist/utils/ssrf-guard.js.map +1 -1
  130. package/package.json +4 -3
@@ -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. */
@@ -321,157 +354,209 @@ if (transportMode === 'http') {
321
354
  }
322
355
 
323
356
  const httpServer = http.createServer(async (req, res) => {
324
- const url = new URL(req.url ?? '/', `http://localhost:${port}`);
325
- const method = req.method ?? 'GET';
326
-
327
- /* ── Authentication ─────────────────────────────────────────────
328
- * Every endpoint except /health, /ready{{#if hasOAuth}}, and the public
329
- * OAuth metadata{{/if}} requires a valid bearer token. With MCP_AUTH_TOKEN
330
- * unset the server fails closed (401) unless MCP_ALLOW_UNAUTHENTICATED=true.
331
- * This is the in-container line of defense; the hosting backend also
332
- * validates before proxying. */
333
- const isPublicPath =
334
- url.pathname === '/health' ||
335
- url.pathname === '/ready'{{#if hasOAuth}} ||
336
- url.pathname === '/.well-known/oauth-authorization-server'{{/if}};
337
- if (!isPublicPath && !isAuthorized(req, authToken)) {
338
- log('error', 'Unauthorized request rejected', { path: url.pathname });
339
- res.writeHead(401, {
340
- 'Content-Type': 'application/json',
341
- 'WWW-Authenticate': 'Bearer',
342
- });
343
- res.end(JSON.stringify({ error: 'Unauthorized: missing or invalid bearer token' }));
344
- return;
345
- }
357
+ try {
358
+ const url = new URL(req.url ?? '/', `http://localhost:${port}`);
359
+ const method = req.method ?? 'GET';
360
+
361
+ /* ── Authentication ─────────────────────────────────────────────
362
+ * Every endpoint except /health, /ready{{#if hasOAuth}}, and the public
363
+ * OAuth metadata{{/if}} requires a valid bearer token. With MCP_AUTH_TOKEN
364
+ * unset the server fails closed (401) unless MCP_ALLOW_UNAUTHENTICATED=true.
365
+ * This is the in-container line of defense; the hosting backend also
366
+ * validates before proxying. */
367
+ const isPublicPath =
368
+ url.pathname === '/health' ||
369
+ url.pathname === '/ready'{{#if hasOAuth}} ||
370
+ url.pathname === '/.well-known/oauth-authorization-server'{{/if}};
371
+ if (!isPublicPath && !isAuthorized(req, authToken)) {
372
+ log('error', 'Unauthorized request rejected', { path: url.pathname });
373
+ res.writeHead(401, {
374
+ 'Content-Type': 'application/json',
375
+ 'WWW-Authenticate': 'Bearer',
376
+ });
377
+ res.end(JSON.stringify({ error: 'Unauthorized: missing or invalid bearer token' }));
378
+ return;
379
+ }
346
380
 
347
- /* ── Routing-header observability (SEP-2243, additive) ─────────── */
348
- const mcpMethod = req.headers['mcp-method'];
349
- const mcpName = req.headers['mcp-name'];
350
- if (mcpMethod || mcpName) {
351
- log('info', 'mcp-meta-headers', { mcpMethod, mcpName, path: url.pathname });
352
- }
381
+ /* ── Routing-header observability (SEP-2243, additive) ─────────── */
382
+ const mcpMethod = req.headers['mcp-method'];
383
+ const mcpName = req.headers['mcp-name'];
384
+ if (mcpMethod || mcpName) {
385
+ log('info', 'mcp-meta-headers', { mcpMethod, mcpName, path: url.pathname });
386
+ }
353
387
 
354
388
  {{#if hasOAuth}}
355
- /* ── OAuth Authorization Server Metadata (RFC 8414) ─────────────
356
- * Public discovery endpoint (the 2026-07-28 auth changes clarify
357
- * .well-known discovery; DCR is retained, not replaced). */
358
- if (url.pathname === '/.well-known/oauth-authorization-server') {
359
- if (method !== 'GET') {
360
- res.writeHead(405, { Allow: 'GET' });
361
- res.end();
362
- return;
363
- }
364
- const authorizationUrl = config.oauth2AuthorizationUrl;
365
- const tokenUrl = config.oauth2TokenUrl;
366
- if (!authorizationUrl || !tokenUrl) {
367
- sendJson(res, 503, { error: 'OAuth is not configured' });
389
+ /* ── OAuth Authorization Server Metadata (RFC 8414) ─────────────
390
+ * Public discovery endpoint (the 2026-07-28 auth changes clarify
391
+ * .well-known discovery; DCR is retained, not replaced). */
392
+ if (url.pathname === '/.well-known/oauth-authorization-server') {
393
+ if (method !== 'GET') {
394
+ res.writeHead(405, { Allow: 'GET' });
395
+ res.end();
396
+ return;
397
+ }
398
+ const authorizationUrl = config.oauth2AuthorizationUrl;
399
+ const tokenUrl = config.oauth2TokenUrl;
400
+ if (!authorizationUrl || !tokenUrl) {
401
+ sendJson(res, 503, { error: 'OAuth is not configured' });
402
+ return;
403
+ }
404
+ const proto = req.headers['x-forwarded-proto'] === 'https' ? 'https' : 'http';
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
+ }
451
+ sendJson(
452
+ res,
453
+ 200,
454
+ getAuthServerMetadata(
455
+ {
456
+ clientId: config.oauth2ClientId ?? '',
457
+ clientSecret: config.oauth2ClientSecret,
458
+ authorizationUrl,
459
+ tokenUrl,
460
+ scopes: config.oauth2Scopes,
461
+ redirectUri: config.oauth2RedirectUri ?? '',
462
+ },
463
+ issuer,
464
+ ),
465
+ );
368
466
  return;
369
467
  }
370
- const proto = req.headers['x-forwarded-proto'] === 'https' ? 'https' : 'http';
371
- const issuer = allowedOrigin ?? `${proto}://${req.headers.host ?? 'localhost'}`;
372
- sendJson(
373
- res,
374
- 200,
375
- getAuthServerMetadata(
376
- {
377
- clientId: config.oauth2ClientId ?? '',
378
- clientSecret: config.oauth2ClientSecret,
379
- authorizationUrl,
380
- tokenUrl,
381
- scopes: config.oauth2Scopes,
382
- redirectUri: config.oauth2RedirectUri ?? '',
383
- },
384
- issuer,
385
- ),
386
- );
387
- return;
388
- }
389
468
 
390
469
  {{/if}}
391
470
  {{#if hasAsyncTools}}
392
- /* ── Task management REST routes (retained during the grace period) ── */
393
- if (handleTaskSse(req, res, url)) return;
394
- if (handleTaskRoutes(req, res, url)) return;
471
+ /* ── Task management REST routes (retained during the grace period) ── */
472
+ if (handleTaskSse(req, res, url)) return;
473
+ if (handleTaskRoutes(req, res, url)) return;
395
474
 
396
475
  {{/if}}
397
- if (url.pathname === '/mcp') {
398
- /* Origin validation (DNS-rebinding protection per MCP spec) */
399
- const origin = req.headers.origin;
400
- if (allowedOrigin) {
401
- if (origin && origin !== allowedOrigin) {
402
- log('error', 'Origin rejected', { origin, allowedOrigin });
476
+ if (url.pathname === '/mcp') {
477
+ /* Origin validation (DNS-rebinding protection per MCP spec) */
478
+ const origin = req.headers.origin;
479
+ if (allowedOrigin) {
480
+ if (origin && origin !== allowedOrigin) {
481
+ log('error', 'Origin rejected', { origin, allowedOrigin });
482
+ res.writeHead(403, { 'Content-Type': 'application/json' });
483
+ res.end(JSON.stringify({ error: 'Forbidden: invalid origin' }));
484
+ return;
485
+ }
486
+ } else if (origin) {
487
+ /* No allowlist configured but Origin header present — reject cross-origin requests */
488
+ log('error', 'Cross-origin request rejected (ALLOWED_ORIGIN not set)', { origin });
403
489
  res.writeHead(403, { 'Content-Type': 'application/json' });
404
- res.end(JSON.stringify({ error: 'Forbidden: invalid origin' }));
405
- return;
406
- }
407
- } else if (origin) {
408
- /* No allowlist configured but Origin header present — reject cross-origin requests */
409
- log('error', 'Cross-origin request rejected (ALLOWED_ORIGIN not set)', { origin });
410
- res.writeHead(403, { 'Content-Type': 'application/json' });
411
- res.end(JSON.stringify({ error: 'Forbidden: ALLOWED_ORIGIN not configured' }));
412
- return;
413
- }
414
-
415
- /* POST carries a JSON-RPC body: peek for server/discover + tasks/* and
416
- * forward the parsed body to the transport. GET/DELETE (SSE stream /
417
- * session teardown) pass straight through. */
418
- if (method === 'POST') {
419
- const raw = await readBody(req);
420
- if (raw === null) {
421
- rpcError(res, null, -32600, 'Request body too large');
422
- return;
423
- }
424
- if (raw.length === 0) {
425
- rpcError(res, null, -32600, 'Empty request body');
426
- return;
427
- }
428
- let parsed: unknown;
429
- try {
430
- parsed = JSON.parse(raw.toString('utf-8'));
431
- } catch {
432
- rpcError(res, null, -32700, 'Parse error');
490
+ res.end(JSON.stringify({ error: 'Forbidden: ALLOWED_ORIGIN not configured' }));
433
491
  return;
434
492
  }
435
493
 
436
- /* Intercept single-object JSON-RPC extension methods the SDK doesn't
437
- * dispatch yet. Batches are forwarded untouched. */
438
- if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
439
- const rpcMethod = (parsed as { method?: unknown }).method;
440
- const rpcId = (parsed as { id?: unknown }).id;
441
- if (rpcMethod === 'server/discover') {
442
- sendJson(res, 200, buildDiscoverResult(rpcId));
494
+ /* POST carries a JSON-RPC body: peek for server/discover + tasks/* and
495
+ * forward the parsed body to the transport. GET/DELETE (SSE stream /
496
+ * session teardown) pass straight through. */
497
+ if (method === 'POST') {
498
+ const raw = await readBody(req);
499
+ if (raw === null) {
500
+ rpcError(res, null, -32600, 'Request body too large');
443
501
  return;
444
502
  }
445
- {{#if hasAsyncTools}}
446
- if (typeof rpcMethod === 'string' && rpcMethod.startsWith('tasks/')) {
447
- const handled = handleTaskRpc(rpcMethod, (parsed as { params?: unknown }).params, rpcId);
448
- if (handled) {
449
- sendJson(res, 200, handled);
503
+ if (raw.length === 0) {
504
+ rpcError(res, null, -32600, 'Empty request body');
505
+ return;
506
+ }
507
+ let parsed: unknown;
508
+ try {
509
+ parsed = JSON.parse(raw.toString('utf-8'));
510
+ } catch {
511
+ rpcError(res, null, -32700, 'Parse error');
512
+ return;
513
+ }
514
+
515
+ /* Intercept single-object JSON-RPC extension methods the SDK doesn't
516
+ * dispatch yet. Batches are forwarded untouched. */
517
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
518
+ const rpcMethod = (parsed as { method?: unknown }).method;
519
+ const rpcId = (parsed as { id?: unknown }).id;
520
+ if (rpcMethod === 'server/discover') {
521
+ sendJson(res, 200, buildDiscoverResult(rpcId));
450
522
  return;
451
523
  }
452
- }
524
+ {{#if hasAsyncTools}}
525
+ if (typeof rpcMethod === 'string' && rpcMethod.startsWith('tasks/')) {
526
+ const handled = handleTaskRpc(rpcMethod, (parsed as { params?: unknown }).params, rpcId);
527
+ if (handled) {
528
+ sendJson(res, 200, handled);
529
+ return;
530
+ }
531
+ }
453
532
  {{/if}}
454
- }
533
+ }
455
534
 
456
- await dispatchToTransport(req, res, parsed);
457
- return;
458
- }
535
+ await dispatchToTransport(req, res, parsed);
536
+ return;
537
+ }
459
538
 
460
- await dispatchToTransport(req, res);
461
- } else if (url.pathname === '/health') {
462
- res.writeHead(200, { 'Content-Type': 'application/json' });
463
- res.end(JSON.stringify({ status: 'ok' }));
464
- } else if (url.pathname === '/ready') {
465
- if (isReady) {
539
+ await dispatchToTransport(req, res);
540
+ } else if (url.pathname === '/health') {
466
541
  res.writeHead(200, { 'Content-Type': 'application/json' });
467
- res.end(JSON.stringify({ status: 'ready' }));
542
+ res.end(JSON.stringify({ status: 'ok' }));
543
+ } else if (url.pathname === '/ready') {
544
+ if (isReady) {
545
+ res.writeHead(200, { 'Content-Type': 'application/json' });
546
+ res.end(JSON.stringify({ status: 'ready' }));
547
+ } else {
548
+ res.writeHead(503, { 'Content-Type': 'application/json' });
549
+ res.end(JSON.stringify({ status: 'not ready' }));
550
+ }
468
551
  } else {
469
- res.writeHead(503, { 'Content-Type': 'application/json' });
470
- res.end(JSON.stringify({ status: 'not ready' }));
552
+ res.writeHead(404);
553
+ res.end('Not found');
554
+ }
555
+ } catch (e) {
556
+ log('error', 'Unhandled error in request handler', { err: String(e) });
557
+ if (!res.headersSent) {
558
+ sendJson(res, 500, { error: 'Internal server error' });
471
559
  }
472
- } else {
473
- res.writeHead(404);
474
- res.end('Not found');
475
560
  }
476
561
  });
477
562
 
@@ -489,8 +574,8 @@ if (transportMode === 'http') {
489
574
 
490
575
  /* ── Graceful shutdown ─────────────────────────────────────────────── */
491
576
 
492
- process.on('SIGTERM', () => {
493
- log('info', 'SIGTERM received, shutting down');
577
+ const shutdown = (signal: string) => {
578
+ log('info', `${signal} received, shutting down`);
494
579
  isReady = false;
495
580
  clearInterval(reaper);
496
581
 
@@ -514,7 +599,10 @@ if (transportMode === 'http') {
514
599
  log('error', 'Shutdown timed out after 5 s, forcing exit');
515
600
  process.exit(1);
516
601
  }, 5000).unref();
517
- });
602
+ };
603
+
604
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
605
+ process.on('SIGINT', () => shutdown('SIGINT'));
518
606
  } else {
519
607
  const server = createMcpServer(config);
520
608
  const transport = new StdioServerTransport();
@@ -131,7 +131,8 @@ export function handleTaskRoutes(
131
131
 
132
132
  const statusParam = url.searchParams.get('status') ?? undefined;
133
133
  const limitParam = url.searchParams.get('limit');
134
- const limit = limitParam ? parseInt(limitParam, 10) : 50;
134
+ const rawLimit = parseInt(limitParam ?? '', 10);
135
+ const limit = Number.isInteger(rawLimit) && rawLimit > 0 && rawLimit <= 1000 ? rawLimit : 50;
135
136
 
136
137
  if (statusParam && !VALID_STATUSES.has(statusParam)) {
137
138
  sendJson(res, 400, { error: `Invalid status filter: ${statusParam}` });
@@ -35,10 +35,12 @@ const MAX_TASKS = 1000;
35
35
  export const taskEvents = new EventEmitter();
36
36
 
37
37
  export function createTask(toolName: string): Task {
38
- // Evict oldest if at capacity
38
+ // Evict a terminal task first; only fall back to the oldest (by insertion order) if none exist.
39
39
  if (tasks.size >= MAX_TASKS) {
40
- const oldest = tasks.keys().next().value;
41
- if (oldest) tasks.delete(oldest);
40
+ const terminalKey = [...tasks.entries()].find(
41
+ ([, t]) => t.status === 'completed' || t.status === 'failed' || t.status === 'cancelled',
42
+ )?.[0] ?? tasks.keys().next().value;
43
+ if (terminalKey !== undefined) tasks.delete(terminalKey);
42
44
  }
43
45
 
44
46
  const task: Task = {
@@ -37,7 +37,7 @@ function stopHeartbeatIfEmpty(): void {
37
37
  function broadcast(eventName: string, data: unknown, taskId: string): void {
38
38
  const payload = `event: ${eventName}\ndata: ${JSON.stringify(data)}\n\n`;
39
39
  for (const conn of connections) {
40
- if (conn.res.writableEnded) continue;
40
+ if (conn.res.writableEnded) { connections.delete(conn); continue; }
41
41
  if (conn.taskIdFilter && conn.taskIdFilter !== taskId) continue;
42
42
  conn.res.write(payload);
43
43
  }
@@ -83,6 +83,14 @@ export function handleTaskSse(
83
83
 
84
84
  const taskIdFilter = url.searchParams.get('taskId') ?? null;
85
85
 
86
+ const rawMax = parseInt(process.env.MCP_MAX_SSE_CONNECTIONS ?? '', 10);
87
+ const maxConnections = Number.isInteger(rawMax) && rawMax > 0 ? rawMax : 500;
88
+ if (connections.size >= maxConnections) {
89
+ res.writeHead(503, { 'Content-Type': 'application/json' });
90
+ res.end(JSON.stringify({ error: 'SSE connection limit reached' }));
91
+ return true;
92
+ }
93
+
86
94
  res.writeHead(200, {
87
95
  'Content-Type': 'text/event-stream',
88
96
  'Cache-Control': 'no-cache',
@@ -31,9 +31,9 @@ const controlSchema = {
31
31
  };
32
32
 
33
33
  const inputSchema = ({{{inputSchemaCode}}}).extend(controlSchema);
34
- {{#if outputSchemaCode}}
34
+ {{#if outputSchemaCode}}{{#unless isAsync}}
35
35
  const outputSchema = {{{outputSchemaCode}}};
36
- {{/if}}
36
+ {{/unless}}{{/if}}
37
37
 
38
38
  /** Execute the HTTP request for this tool and return the (optionally filtered) data. */
39
39
  async function runRequest(rawInput: Record<string, unknown>, config: AppConfig): Promise<unknown> {
@@ -86,9 +86,9 @@ export function register(server: McpServer, config: AppConfig): void {
86
86
  title: '{{title}}',
87
87
  description: `{{{description}}}`,
88
88
  inputSchema,
89
- {{#if outputSchemaCode}}
89
+ {{#if outputSchemaCode}}{{#unless isAsync}}
90
90
  outputSchema,
91
- {{/if}}
91
+ {{/unless}}{{/if}}
92
92
  {{#if annotations}}
93
93
  annotations: {
94
94
  {{#if annotations.readOnlyHint}}
@@ -136,6 +136,19 @@ export function register(server: McpServer, config: AppConfig): void {
136
136
  text: JSON.stringify(result, null, 2),
137
137
  },
138
138
  ],
139
+ {{#if outputSchemaCode}}{{#unless isAsync}}
140
+ // MCP SDK validates structuredContent against outputSchema when set.
141
+ // wrapArrayRoot (schema-converter.ts) wraps ONLY type:array schemas as
142
+ // { type:object, properties:{ items }, required:[items] } and passes
143
+ // objects AND scalars through bare. Mirror that exactly at runtime:
144
+ // - array → { items: result } (matches the wrapped object schema)
145
+ // - object → result as-is (bare object, no wrap)
146
+ // - scalar → result as-is (bare scalar; cast required for TS)
147
+ structuredContent:
148
+ Array.isArray(result)
149
+ ? { items: result }
150
+ : (result as unknown as Record<string, unknown>),
151
+ {{/unless}}{{/if}}
139
152
  };
140
153
  } catch (error) {
141
154
  const message = error instanceof Error ? error.message : String(error);
@@ -7,18 +7,26 @@ export interface AppConfig {
7
7
  baseUrl: string;
8
8
  {{#each authSchemes}}
9
9
  {{#if (eq type "apiKey")}}
10
+ {{#unless (eq emitApiKeyValue false)}}
10
11
  apiKey?: string;
12
+ {{/unless}}
11
13
  {{#if (eq in "query")}}
14
+ {{#unless (eq emitApiKeyQueryName false)}}
12
15
  /** Query-parameter name the API key is appended under (apiKey-in-query auth). */
13
16
  apiKeyQueryName?: string;
17
+ {{/unless}}
14
18
  {{/if}}
15
19
  {{/if}}
16
20
  {{#if (eq type "http-bearer")}}
21
+ {{#unless (eq emitBearer false)}}
17
22
  bearerToken?: string;
23
+ {{/unless}}
18
24
  {{/if}}
19
25
  {{#if (eq type "http-basic")}}
26
+ {{#unless (eq emitBasic false)}}
20
27
  basicUsername?: string;
21
28
  basicPassword?: string;
29
+ {{/unless}}
22
30
  {{/if}}
23
31
  {{/each}}
24
32
  maxRetries: number;
@@ -29,6 +37,17 @@ export interface AppConfig {
29
37
 
30
38
  type EnvLike = Record<string, string | undefined>;
31
39
 
40
+ const posInt = (v: string | undefined, d: number): number => {
41
+ const n = parseInt(v ?? '', 10);
42
+ return Number.isInteger(n) && n > 0 ? n : d;
43
+ };
44
+
45
+ /** Like posInt but accepts 0 — useful where 0 is a meaningful "disable" value. */
46
+ const nonNegInt = (v: string | undefined, d: number): number => {
47
+ const n = parseInt(v ?? '', 10);
48
+ return Number.isInteger(n) && n >= 0 ? n : d;
49
+ };
50
+
32
51
  /**
33
52
  * Resolve the upstream base URL with optional named environments.
34
53
  * `MCP_ENVIRONMENTS` is a JSON object mapping name → base URL; `API_ENVIRONMENT`
@@ -72,9 +91,13 @@ export function loadConfig(env: EnvLike): AppConfig {
72
91
  baseUrl: resolveBaseUrl(env),
73
92
  {{#each authSchemes}}
74
93
  {{#if (eq type "apiKey")}}
94
+ {{#unless (eq emitApiKeyValue false)}}
75
95
  apiKey: env.{{envVarName}},
96
+ {{/unless}}
76
97
  {{#if (eq in "query")}}
98
+ {{#unless (eq emitApiKeyQueryName false)}}
77
99
  apiKeyQueryName: {{{json headerName}}},
100
+ {{/unless}}
78
101
  {{/if}}
79
102
  {{/if}}
80
103
  {{#if (eq type "http-bearer")}}
@@ -85,8 +108,8 @@ export function loadConfig(env: EnvLike): AppConfig {
85
108
  basicPassword: env.BASIC_PASSWORD,
86
109
  {{/if}}
87
110
  {{/each}}
88
- maxRetries: parseInt(env.MAX_RETRIES ?? '3', 10),
89
- requestIntervalMs: parseInt(env.REQUEST_INTERVAL_MS ?? '100', 10),
111
+ maxRetries: nonNegInt(env.MAX_RETRIES, 3),
112
+ requestIntervalMs: nonNegInt(env.REQUEST_INTERVAL_MS, 100),
90
113
  defaultHeaders: loadDefaultHeaders(env),
91
114
  };
92
115
  }
@@ -216,8 +216,12 @@ async function handleToolCall(
216
216
  if (!parsed.success) {
217
217
  return err(id, -32602, `Invalid arguments for ${name}: ${parsed.error.message}`);
218
218
  }
219
- const result = await entry.handler(parsed.data as Record<string, unknown>, config);
220
- return ok(id, result);
219
+ try {
220
+ const result = await entry.handler(parsed.data as Record<string, unknown>, config);
221
+ return ok(id, result);
222
+ } catch {
223
+ return err(id, -32000, 'Tool execution error');
224
+ }
221
225
  }
222
226
 
223
227
  /* Map `items` through `fn` with at most `limit` in flight at once, preserving
@@ -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
  // ---------------------------------------------------------------------------