@mcpmake/core 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/dist/analyzer/dom-parser.d.ts +35 -0
  2. package/dist/analyzer/dom-parser.d.ts.map +1 -1
  3. package/dist/analyzer/dom-parser.js +107 -6
  4. package/dist/analyzer/dom-parser.js.map +1 -1
  5. package/dist/analyzer/goal-crawler.d.ts +2 -1
  6. package/dist/analyzer/goal-crawler.d.ts.map +1 -1
  7. package/dist/analyzer/goal-crawler.js +66 -17
  8. package/dist/analyzer/goal-crawler.js.map +1 -1
  9. package/dist/analyzer/semantic-analyzer.d.ts +1 -1
  10. package/dist/analyzer/semantic-analyzer.d.ts.map +1 -1
  11. package/dist/analyzer/semantic-analyzer.js +15 -16
  12. package/dist/analyzer/semantic-analyzer.js.map +1 -1
  13. package/dist/analyzer/site-crawler.d.ts.map +1 -1
  14. package/dist/analyzer/site-crawler.js +122 -16
  15. package/dist/analyzer/site-crawler.js.map +1 -1
  16. package/dist/config/mcpmake-config.d.ts.map +1 -1
  17. package/dist/config/mcpmake-config.js +5 -0
  18. package/dist/config/mcpmake-config.js.map +1 -1
  19. package/dist/emitter/code-writer.d.ts +1 -0
  20. package/dist/emitter/code-writer.d.ts.map +1 -1
  21. package/dist/emitter/code-writer.js +79 -12
  22. package/dist/emitter/code-writer.js.map +1 -1
  23. package/dist/emitter/index.d.ts +8 -0
  24. package/dist/emitter/index.d.ts.map +1 -1
  25. package/dist/emitter/index.js +113 -6
  26. package/dist/emitter/index.js.map +1 -1
  27. package/dist/emitter/project-scaffolder.d.ts +13 -0
  28. package/dist/emitter/project-scaffolder.d.ts.map +1 -1
  29. package/dist/emitter/project-scaffolder.js +29 -0
  30. package/dist/emitter/project-scaffolder.js.map +1 -1
  31. package/dist/emitter/python-template-loader.d.ts.map +1 -1
  32. package/dist/emitter/python-template-loader.js +10 -0
  33. package/dist/emitter/python-template-loader.js.map +1 -1
  34. package/dist/emitter/python-templates/dockerfile.hbs +4 -1
  35. package/dist/emitter/python-templates/server.py.hbs +82 -8
  36. package/dist/emitter/site-scaffolder.d.ts.map +1 -1
  37. package/dist/emitter/site-scaffolder.js +28 -1
  38. package/dist/emitter/site-scaffolder.js.map +1 -1
  39. package/dist/emitter/site-templates/browser-manager.ts.hbs +104 -34
  40. package/dist/emitter/site-templates/dockerfile.hbs +13 -4
  41. package/dist/emitter/site-templates/env.example.hbs +8 -0
  42. package/dist/emitter/site-templates/server-main-http.ts.hbs +77 -6
  43. package/dist/emitter/site-templates/telemetry.ts.hbs +117 -0
  44. package/dist/emitter/site-templates/tool-handler-form.ts.hbs +10 -8
  45. package/dist/emitter/template-loader.d.ts.map +1 -1
  46. package/dist/emitter/template-loader.js +9 -0
  47. package/dist/emitter/template-loader.js.map +1 -1
  48. package/dist/emitter/templates/auth-provider.ts.hbs +86 -21
  49. package/dist/emitter/templates/config.ts.hbs +29 -0
  50. package/dist/emitter/templates/dockerfile.hbs +12 -4
  51. package/dist/emitter/templates/env.example.hbs +6 -0
  52. package/dist/emitter/templates/http-executor.ts.hbs +150 -7
  53. package/dist/emitter/templates/oauth.ts.hbs +19 -65
  54. package/dist/emitter/templates/server-main-http.ts.hbs +93 -12
  55. package/dist/emitter/templates/task-handlers.ts.hbs +40 -1
  56. package/dist/emitter/templates/tool-handler.ts.hbs +37 -2
  57. package/dist/emitter/templates/tool-test.ts.hbs +9 -1
  58. package/dist/emitter/worker-templates/config.ts.hbs +7 -0
  59. package/dist/emitter/worker-templates/tool-handler.ts.hbs +37 -2
  60. package/dist/emitter/worker-templates/worker.ts.hbs +119 -25
  61. package/dist/generator/spec-generator.d.ts.map +1 -1
  62. package/dist/generator/spec-generator.js +9 -54
  63. package/dist/generator/spec-generator.js.map +1 -1
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +1 -0
  67. package/dist/index.js.map +1 -1
  68. package/dist/llm/anthropic-provider.d.ts +14 -0
  69. package/dist/llm/anthropic-provider.d.ts.map +1 -0
  70. package/dist/llm/anthropic-provider.js +50 -0
  71. package/dist/llm/anthropic-provider.js.map +1 -0
  72. package/dist/llm/index.d.ts +19 -0
  73. package/dist/llm/index.d.ts.map +1 -0
  74. package/dist/llm/index.js +73 -0
  75. package/dist/llm/index.js.map +1 -0
  76. package/dist/llm/openai-provider.d.ts +46 -0
  77. package/dist/llm/openai-provider.d.ts.map +1 -0
  78. package/dist/llm/openai-provider.js +221 -0
  79. package/dist/llm/openai-provider.js.map +1 -0
  80. package/dist/llm/types.d.ts +61 -0
  81. package/dist/llm/types.d.ts.map +1 -0
  82. package/dist/llm/types.js +16 -0
  83. package/dist/llm/types.js.map +1 -0
  84. package/dist/parser/har-filter.d.ts +7 -0
  85. package/dist/parser/har-filter.d.ts.map +1 -1
  86. package/dist/parser/har-filter.js +107 -1
  87. package/dist/parser/har-filter.js.map +1 -1
  88. package/dist/parser/index.d.ts +1 -1
  89. package/dist/parser/index.d.ts.map +1 -1
  90. package/dist/parser/index.js +1 -1
  91. package/dist/parser/index.js.map +1 -1
  92. package/dist/parser/openapi-loader.d.ts.map +1 -1
  93. package/dist/parser/openapi-loader.js +36 -2
  94. package/dist/parser/openapi-loader.js.map +1 -1
  95. package/dist/parser/operation-extractor.d.ts.map +1 -1
  96. package/dist/parser/operation-extractor.js +12 -1
  97. package/dist/parser/operation-extractor.js.map +1 -1
  98. package/dist/parser/overlay-loader.d.ts.map +1 -1
  99. package/dist/parser/overlay-loader.js +11 -2
  100. package/dist/parser/overlay-loader.js.map +1 -1
  101. package/dist/parser/postman-loader.d.ts.map +1 -1
  102. package/dist/parser/postman-loader.js +6 -1
  103. package/dist/parser/postman-loader.js.map +1 -1
  104. package/dist/parser/schema-converter.d.ts +13 -2
  105. package/dist/parser/schema-converter.d.ts.map +1 -1
  106. package/dist/parser/schema-converter.js +64 -42
  107. package/dist/parser/schema-converter.js.map +1 -1
  108. package/dist/plugins/loader.d.ts +5 -1
  109. package/dist/plugins/loader.d.ts.map +1 -1
  110. package/dist/plugins/loader.js +39 -10
  111. package/dist/plugins/loader.js.map +1 -1
  112. package/dist/pricing.d.ts +5 -1
  113. package/dist/pricing.d.ts.map +1 -1
  114. package/dist/pricing.js +7 -3
  115. package/dist/pricing.js.map +1 -1
  116. package/dist/providers/index.d.ts.map +1 -1
  117. package/dist/providers/index.js +6 -21
  118. package/dist/providers/index.js.map +1 -1
  119. package/dist/recorder/browser-recorder.d.ts.map +1 -1
  120. package/dist/recorder/browser-recorder.js +89 -12
  121. package/dist/recorder/browser-recorder.js.map +1 -1
  122. package/dist/rescan/rescan-scheduler.d.ts.map +1 -1
  123. package/dist/rescan/rescan-scheduler.js +22 -7
  124. package/dist/rescan/rescan-scheduler.js.map +1 -1
  125. package/dist/site-transformer/selector-healer.d.ts +1 -1
  126. package/dist/site-transformer/selector-healer.d.ts.map +1 -1
  127. package/dist/site-transformer/selector-healer.js +17 -18
  128. package/dist/site-transformer/selector-healer.js.map +1 -1
  129. package/dist/site-transformer/tool-generator.d.ts.map +1 -1
  130. package/dist/site-transformer/tool-generator.js +90 -22
  131. package/dist/site-transformer/tool-generator.js.map +1 -1
  132. package/dist/transformer/auth-detector.d.ts.map +1 -1
  133. package/dist/transformer/auth-detector.js +24 -5
  134. package/dist/transformer/auth-detector.js.map +1 -1
  135. package/dist/transformer/har-schema-inferrer.d.ts.map +1 -1
  136. package/dist/transformer/har-schema-inferrer.js +2 -0
  137. package/dist/transformer/har-schema-inferrer.js.map +1 -1
  138. package/dist/transformer/har-to-operations.d.ts +1 -0
  139. package/dist/transformer/har-to-operations.d.ts.map +1 -1
  140. package/dist/transformer/har-to-operations.js +42 -10
  141. package/dist/transformer/har-to-operations.js.map +1 -1
  142. package/dist/transformer/llm-namer.d.ts.map +1 -1
  143. package/dist/transformer/llm-namer.js +10 -13
  144. package/dist/transformer/llm-namer.js.map +1 -1
  145. package/dist/transformer/naming.d.ts.map +1 -1
  146. package/dist/transformer/naming.js +16 -8
  147. package/dist/transformer/naming.js.map +1 -1
  148. package/dist/transformer/resource-builder.d.ts.map +1 -1
  149. package/dist/transformer/resource-builder.js +13 -6
  150. package/dist/transformer/resource-builder.js.map +1 -1
  151. package/dist/transformer/stainless-translator.d.ts +6 -0
  152. package/dist/transformer/stainless-translator.d.ts.map +1 -1
  153. package/dist/transformer/stainless-translator.js +18 -1
  154. package/dist/transformer/stainless-translator.js.map +1 -1
  155. package/dist/transformer/tool-builder.d.ts.map +1 -1
  156. package/dist/transformer/tool-builder.js +141 -14
  157. package/dist/transformer/tool-builder.js.map +1 -1
  158. package/dist/types/index.d.ts +79 -0
  159. package/dist/types/index.d.ts.map +1 -1
  160. package/dist/types/site.d.ts +19 -1
  161. package/dist/types/site.d.ts.map +1 -1
  162. package/dist/utils/fail.d.ts.map +1 -1
  163. package/dist/utils/fail.js +22 -4
  164. package/dist/utils/fail.js.map +1 -1
  165. package/dist/utils/json-extract.d.ts +21 -0
  166. package/dist/utils/json-extract.d.ts.map +1 -0
  167. package/dist/utils/json-extract.js +67 -0
  168. package/dist/utils/json-extract.js.map +1 -0
  169. package/dist/utils/model-resolver.d.ts.map +1 -1
  170. package/dist/utils/model-resolver.js +6 -0
  171. package/dist/utils/model-resolver.js.map +1 -1
  172. package/dist/utils/sanitize.d.ts +63 -1
  173. package/dist/utils/sanitize.d.ts.map +1 -1
  174. package/dist/utils/sanitize.js +147 -2
  175. package/dist/utils/sanitize.js.map +1 -1
  176. package/dist/utils/ssrf-guard.d.ts +10 -0
  177. package/dist/utils/ssrf-guard.d.ts.map +1 -0
  178. package/dist/utils/ssrf-guard.js +130 -0
  179. package/dist/utils/ssrf-guard.js.map +1 -0
  180. package/dist/utils/watcher.d.ts +17 -1
  181. package/dist/utils/watcher.d.ts.map +1 -1
  182. package/dist/utils/watcher.js +53 -21
  183. package/dist/utils/watcher.js.map +1 -1
  184. package/package.json +2 -1
@@ -6,6 +6,7 @@
6
6
  import { chromium } from 'playwright';
7
7
  import type { Browser, BrowserContext, Page } from 'playwright';
8
8
  import type { BrowserConfig } from './config.js';
9
+ import * as telemetry from './telemetry.js';
9
10
 
10
11
  interface BrowserSession {
11
12
  context: BrowserContext;
@@ -18,6 +19,16 @@ const sessions = new Map<string, BrowserSession>();
18
19
  let idleTimer: ReturnType<typeof setInterval> | undefined;
19
20
  let config: BrowserConfig;
20
21
 
22
+ // In-flight session creations, keyed by session id. Single-flighting per id
23
+ // means concurrent no-ID calls (which all resolve to DEFAULT_SESSION) and
24
+ // repeated calls with the same explicit id share one Chromium context instead
25
+ // of racing to create several.
26
+ const sessionCreations = new Map<string, Promise<{ page: Page; sessionId: string }>>();
27
+
28
+ // Single-flight the browser launch so concurrent cold starts can't spawn
29
+ // multiple Chromium processes.
30
+ let browserLaunch: Promise<Browser> | undefined;
31
+
21
32
  const DEFAULT_SESSION = '__default__';
22
33
 
23
34
  /**
@@ -78,10 +89,37 @@ export async function resolveSelector(
78
89
  }
79
90
  }
80
91
 
92
+ /**
93
+ * Launch the shared browser, single-flighting concurrent cold starts so two
94
+ * simultaneous requests can never spawn two Chromium processes.
95
+ */
96
+ async function getBrowser(): Promise<Browser> {
97
+ if (browser && browser.isConnected()) return browser;
98
+ if (browserLaunch) return browserLaunch;
99
+
100
+ // Only disable sandbox when running as root in a container.
101
+ const isRoot = process.getuid?.() === 0;
102
+ browserLaunch = chromium
103
+ .launch({
104
+ headless: config.headless,
105
+ args: isRoot ? ['--no-sandbox', '--disable-setuid-sandbox'] : [],
106
+ })
107
+ .then((b) => {
108
+ browser = b;
109
+ return b;
110
+ })
111
+ .finally(() => {
112
+ browserLaunch = undefined;
113
+ });
114
+ return browserLaunch;
115
+ }
116
+
81
117
  /**
82
118
  * Get or create a browser session.
83
119
  * If sessionId is provided, reuses an existing session or creates one with that ID.
84
- * If not, creates a new session with a generated ID.
120
+ * If not, reuses (or creates) a single shared default session under a fixed ID,
121
+ * so repeat no-ID calls and `take_screenshot` without an ID all hit the same
122
+ * context instead of leaking a new Chromium context per call.
85
123
  * Throws if the session pool is at capacity (maxSessions).
86
124
  */
87
125
  export async function getOrCreateSession(sessionId?: string): Promise<{ page: Page; sessionId: string }> {
@@ -95,7 +133,23 @@ export async function getOrCreateSession(sessionId?: string): Promise<{ page: Pa
95
133
  return { page: existing.page, sessionId: id };
96
134
  }
97
135
 
98
- // Check capacity before creating a new session
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
+ const creation = createSession(id).finally(() => {
143
+ sessionCreations.delete(id);
144
+ });
145
+ sessionCreations.set(id, creation);
146
+ return creation;
147
+ }
148
+
149
+ async function createSession(id: string): Promise<{ page: Page; sessionId: string }> {
150
+ // Reserve the capacity slot BEFORE any await so concurrent creations of
151
+ // distinct ids can't all pass the gate and then blow past maxSessions. We
152
+ // insert a placeholder entry now and fill in the real handles below.
99
153
  const maxSessions = config.maxSessions ?? 10;
100
154
  if (sessions.size >= maxSessions) {
101
155
  throw new Error(
@@ -103,38 +157,37 @@ export async function getOrCreateSession(sessionId?: string): Promise<{ page: Pa
103
157
  'Close an existing session before creating a new one.'
104
158
  );
105
159
  }
160
+ const reserved: BrowserSession = {
161
+ context: undefined as unknown as BrowserContext,
162
+ page: undefined as unknown as Page,
163
+ lastAccessedAt: Date.now(),
164
+ };
165
+ sessions.set(id, reserved);
106
166
 
107
- // Launch browser if not running
108
- if (!browser || !browser.isConnected()) {
109
- // Only disable sandbox when running as root in a container
110
- const isRoot = process.getuid?.() === 0;
111
- browser = await chromium.launch({
112
- headless: config.headless,
113
- args: isRoot ? ['--no-sandbox', '--disable-setuid-sandbox'] : [],
167
+ try {
168
+ const activeBrowser = await getBrowser();
169
+ const context = await activeBrowser.newContext({
170
+ viewport: config.viewport,
171
+ ...(config.userAgent ? { userAgent: config.userAgent } : {}),
114
172
  });
115
- }
173
+ const page = await context.newPage();
116
174
 
117
- // Create new session
118
- const context = await browser.newContext({
119
- viewport: config.viewport,
120
- ...(config.userAgent ? { userAgent: config.userAgent } : {}),
121
- });
122
- const page = await context.newPage();
123
-
124
- // Generate final session ID upfront to avoid race conditions
125
- const finalId = id === DEFAULT_SESSION
126
- ? `session_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`
127
- : id;
175
+ reserved.context = context;
176
+ reserved.page = page;
177
+ reserved.lastAccessedAt = Date.now();
128
178
 
129
- sessions.set(finalId, {
130
- context,
131
- page,
132
- lastAccessedAt: Date.now(),
133
- });
179
+ resetIdleTimer();
134
180
 
135
- resetIdleTimer();
181
+ // The browser context is now resident — start billing this session (no-op
182
+ // unless the host injected telemetry env).
183
+ telemetry.sessionStarted(id);
136
184
 
137
- return { page, sessionId: finalId };
185
+ return { page, sessionId: id };
186
+ } catch (err) {
187
+ // Release the reserved slot if context creation failed.
188
+ sessions.delete(id);
189
+ throw err;
190
+ }
138
191
  }
139
192
 
140
193
  /**
@@ -145,8 +198,9 @@ export async function closeSession(sessionId?: string): Promise<void> {
145
198
  const session = sessions.get(id);
146
199
  if (!session) return;
147
200
 
148
- await session.context.close().catch(() => {});
201
+ await session.context?.close().catch(() => {});
149
202
  sessions.delete(id);
203
+ telemetry.sessionEnded(id, 'explicit');
150
204
 
151
205
  // If no sessions remain, close the browser
152
206
  if (sessions.size === 0) {
@@ -161,8 +215,9 @@ export async function closeBrowser(): Promise<void> {
161
215
  clearIdleTimer();
162
216
 
163
217
  for (const [id, session] of sessions) {
164
- await session.context.close().catch(() => {});
218
+ await session.context?.close().catch(() => {});
165
219
  sessions.delete(id);
220
+ telemetry.sessionEnded(id, 'shutdown');
166
221
  }
167
222
 
168
223
  if (browser) {
@@ -207,11 +262,13 @@ function resetIdleTimer(): void {
207
262
  clearIdleTimer();
208
263
  idleTimer = setInterval(() => {
209
264
  const now = Date.now();
210
- // Close idle sessions
265
+ // Close idle sessions. Skip slots still being created (no context yet).
211
266
  for (const [id, session] of sessions) {
267
+ if (!session.context) continue;
212
268
  if (now - session.lastAccessedAt > config.idleTimeoutMs) {
213
269
  session.context.close().catch(() => {});
214
270
  sessions.delete(id);
271
+ telemetry.sessionEnded(id, 'idle');
215
272
  }
216
273
  }
217
274
  // Close browser if no sessions
@@ -228,6 +285,19 @@ function clearIdleTimer(): void {
228
285
  }
229
286
  }
230
287
 
231
- // Clean up on process exit
232
- process.on('SIGTERM', () => closeBrowser());
233
- process.on('SIGINT', () => closeBrowser());
288
+ // Clean up on process exit. Close the browser (which emits a session_end for
289
+ // every still-resident session), then flush telemetry best-effort so the host
290
+ // sees the final boundary instead of waiting out the heartbeat grace.
291
+ async function shutdown(): Promise<void> {
292
+ try {
293
+ await closeBrowser();
294
+ } finally {
295
+ await telemetry.flushOnShutdown();
296
+ }
297
+ }
298
+ process.on('SIGTERM', () => {
299
+ void shutdown();
300
+ });
301
+ process.on('SIGINT', () => {
302
+ void shutdown();
303
+ });
@@ -5,8 +5,14 @@
5
5
  FROM mcr.microsoft.com/playwright:v1.59.1-noble AS builder
6
6
 
7
7
  WORKDIR /app
8
- COPY package.json package-lock.json ./
9
- RUN npm ci
8
+ COPY package.json ./
9
+ # The generator does not emit a package-lock.json, so use `npm install` (not
10
+ # `npm ci`, which fails without a lockfile). --ignore-scripts: a malicious
11
+ # transitive dependency's install lifecycle hook (preinstall/install/postinstall)
12
+ # would otherwise run as root at build time — install-time RCE in the build
13
+ # container. The server's own compile is an explicit `npm run build` step below,
14
+ # so it does not depend on install hooks.
15
+ RUN npm install --ignore-scripts
10
16
  COPY tsconfig.json ./
11
17
  COPY src/ src/
12
18
  RUN npm run build
@@ -20,8 +26,11 @@ ENV PORT=3000
20
26
  ENV HEADLESS=true
21
27
 
22
28
  WORKDIR /app
23
- COPY package.json package-lock.json ./
24
- RUN npm ci --omit=dev
29
+ COPY package.json ./
30
+ # Runtime deps only (no lockfile emitted → `npm install`); --ignore-scripts
31
+ # blocks dependency install hooks from running as root. The Playwright browser
32
+ # binaries ship in the base image, so no install-time download lifecycle is needed.
33
+ RUN npm install --omit=dev --ignore-scripts
25
34
 
26
35
  COPY --from=builder /app/dist dist/
27
36
 
@@ -9,6 +9,14 @@ IDLE_TIMEOUT_MS={{browserConfig.idleTimeoutMs}}
9
9
  VIEWPORT_WIDTH={{browserConfig.viewport.width}}
10
10
  VIEWPORT_HEIGHT={{browserConfig.viewport.height}}
11
11
  # USER_AGENT=
12
+
13
+ # Browser-session telemetry (managed hosting only — leave UNSET to self-host).
14
+ # When the host sets these, the server reports exact browser-session lifetime
15
+ # for metering. It NEVER phones home unless MCPMAKE_TELEMETRY_URL is set.
16
+ # MCPMAKE_TELEMETRY_URL=
17
+ # MCPMAKE_TELEMETRY_TOKEN=
18
+ # MCPMAKE_SERVER_SLUG=
19
+ # MCPMAKE_TELEMETRY_HEARTBEAT_MS=30000
12
20
  {{#if (eq transport "http")}}
13
21
 
14
22
  # Transport: "stdio" or "http"
@@ -1,4 +1,5 @@
1
1
  import http from 'node:http';
2
+ import crypto from 'node:crypto';
2
3
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
4
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
5
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
@@ -14,12 +15,60 @@ function log(level: string, message: string, data?: Record<string, unknown>): vo
14
15
  const config = loadConfig();
15
16
  initBrowserManager(config.browser);
16
17
 
17
- const server = new McpServer({
18
- name: '{{serverName}}',
19
- version: '{{serverVersion}}',
20
- });
18
+ /* Build a fully-registered McpServer. A single McpServer connected to multiple
19
+ * transports concurrently races on its internal transport reference (the API
20
+ * HTTP template documents the same hazard), so HTTP requests get a fresh,
21
+ * stateless server + transport per request instead of sharing one global
22
+ * instance. */
23
+ function createMcpServer(): McpServer {
24
+ const server = new McpServer({
25
+ name: '{{serverName}}',
26
+ version: '{{serverVersion}}',
27
+ });
28
+ registerAllTools(server, config);
29
+ return server;
30
+ }
21
31
 
22
- registerAllTools(server, config);
32
+ /* Bearer authentication — mirrors the API/node server. Constant-time compare of
33
+ * the `Authorization: Bearer <token>` header against MCP_AUTH_TOKEN.
34
+ *
35
+ * Fail CLOSED: when MCP_AUTH_TOKEN is unset, authenticated routes are denied
36
+ * unless the operator opts in with MCP_ALLOW_UNAUTHENTICATED === 'true' (a
37
+ * local/dev escape hatch). We warn once so the open posture is loud. The token
38
+ * is never read from the query string — URLs leak through logs and referrers. */
39
+ let unauthWarned = false;
40
+ function isAuthorized(req: http.IncomingMessage): boolean {
41
+ const expected = process.env.MCP_AUTH_TOKEN;
42
+ if (!expected) {
43
+ if (process.env.MCP_ALLOW_UNAUTHENTICATED === 'true') {
44
+ if (!unauthWarned) {
45
+ unauthWarned = true;
46
+ console.error(
47
+ 'SECURITY: MCP_AUTH_TOKEN is unset and MCP_ALLOW_UNAUTHENTICATED=true — ' +
48
+ 'serving authenticated routes WITHOUT a bearer token. ' +
49
+ 'Set MCP_AUTH_TOKEN before exposing this endpoint.',
50
+ );
51
+ }
52
+ return true;
53
+ }
54
+ if (!unauthWarned) {
55
+ unauthWarned = true;
56
+ console.error(
57
+ 'SECURITY: MCP_AUTH_TOKEN is unset — denying all authenticated routes. ' +
58
+ 'Set MCP_AUTH_TOKEN, or set MCP_ALLOW_UNAUTHENTICATED=true for local/dev only.',
59
+ );
60
+ }
61
+ return false;
62
+ }
63
+ const header = req.headers.authorization;
64
+ if (typeof header !== 'string' || !header.startsWith('Bearer ')) return false;
65
+ const presented = header.slice(7).trim();
66
+ if (!presented) return false;
67
+ const a = Buffer.from(presented);
68
+ const b = Buffer.from(expected);
69
+ if (a.length !== b.length) return false;
70
+ return crypto.timingSafeEqual(a, b);
71
+ }
23
72
 
24
73
  let isReady = false;
25
74
 
@@ -43,6 +92,17 @@ if (process.env.TRANSPORT === 'http') {
43
92
  }
44
93
 
45
94
  if (url.pathname === '/mcp') {
95
+ // Bearer auth (in addition to the origin check below). /health and /ready
96
+ // above stay open for liveness/readiness probes.
97
+ if (!isAuthorized(req)) {
98
+ res.writeHead(401, {
99
+ 'Content-Type': 'application/json',
100
+ 'WWW-Authenticate': 'Bearer',
101
+ });
102
+ res.end(JSON.stringify({ error: 'Unauthorized: missing or invalid bearer token' }));
103
+ return;
104
+ }
105
+
46
106
  if (allowedOrigin) {
47
107
  // Compare host-to-host exactly (DNS-rebinding protection). A substring
48
108
  // match would let "evil-{ALLOWED_ORIGIN}.attacker.com" through.
@@ -72,8 +132,17 @@ if (process.env.TRANSPORT === 'http') {
72
132
  }
73
133
  }
74
134
 
135
+ // Fresh stateless server + transport per request. Both are closed when
136
+ // the response finishes or the connection aborts, so transports and
137
+ // their listeners are never leaked and concurrent requests can't
138
+ // misroute through a shared transport reference.
139
+ const reqServer = createMcpServer();
75
140
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
76
- await server.connect(transport);
141
+ res.on('close', () => {
142
+ void transport.close();
143
+ void reqServer.close();
144
+ });
145
+ await reqServer.connect(transport);
77
146
  await transport.handleRequest(req, res);
78
147
  return;
79
148
  }
@@ -97,6 +166,8 @@ if (process.env.TRANSPORT === 'http') {
97
166
  process.on('SIGTERM', shutdown);
98
167
  process.on('SIGINT', shutdown);
99
168
  } else {
169
+ // Stdio is inherently single-connection, so one long-lived server is correct.
170
+ const server = createMcpServer();
100
171
  const transport = new StdioServerTransport();
101
172
  await server.connect(transport);
102
173
  isReady = true;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Browser-session telemetry for exact metering on managed hosting.
3
+ *
4
+ * NO-OP unless MCPMAKE_TELEMETRY_URL is set — a self-hosted or local server
5
+ * never phones home. When the cloud injects the ingest URL + token + slug, this
6
+ * batches session_start / heartbeat / session_end events and POSTs them so the
7
+ * host can bill the REAL resident browser lifetime instead of approximating it
8
+ * from request latency.
9
+ *
10
+ * Strictly best-effort and isolated: every network/serialization error is
11
+ * swallowed. Telemetry must NEVER affect the MCP server's behavior. Dropped
12
+ * events can only ever UNDER-bill (the host applies a heartbeat grace + an
13
+ * independent proxy-activity clamp), never break a tool call.
14
+ *
15
+ * An OPEN session emits a heartbeat on every flush: an open session means a
16
+ * resident Chromium context, which is the billable cost — independent of how
17
+ * often tools are called. The host treats "last heartbeat + grace" as the close
18
+ * time if a session_end is ever lost (e.g. the server is killed).
19
+ */
20
+
21
+ type TelemetryEventType = 'session_start' | 'heartbeat' | 'session_end';
22
+
23
+ interface TelemetryEvent {
24
+ type: TelemetryEventType;
25
+ sessionId: string;
26
+ ts: number;
27
+ reason?: string;
28
+ }
29
+
30
+ const TELEMETRY_URL = process.env.MCPMAKE_TELEMETRY_URL;
31
+ const TELEMETRY_TOKEN = process.env.MCPMAKE_TELEMETRY_TOKEN ?? '';
32
+ const SERVER_SLUG = process.env.MCPMAKE_SERVER_SLUG ?? '';
33
+ const HEARTBEAT_MS = (() => {
34
+ const n = parseInt(process.env.MCPMAKE_TELEMETRY_HEARTBEAT_MS ?? '30000', 10);
35
+ return Number.isFinite(n) && n >= 1000 ? n : 30000;
36
+ })();
37
+ const POST_TIMEOUT_MS = 5000;
38
+ const MAX_BUFFER = 1000;
39
+
40
+ const ENABLED = Boolean(TELEMETRY_URL) && Boolean(TELEMETRY_TOKEN) && Boolean(SERVER_SLUG);
41
+
42
+ const buffer: TelemetryEvent[] = [];
43
+ const openSessions = new Set<string>();
44
+ let flushTimer: ReturnType<typeof setInterval> | undefined;
45
+
46
+ function pushEvent(ev: TelemetryEvent): void {
47
+ buffer.push(ev);
48
+ // Bound memory if the ingest is unreachable for a long time: keep the newest.
49
+ if (buffer.length > MAX_BUFFER) buffer.splice(0, buffer.length - MAX_BUFFER);
50
+ }
51
+
52
+ function ensureTimer(): void {
53
+ if (!ENABLED || flushTimer) return;
54
+ flushTimer = setInterval(() => {
55
+ void flush();
56
+ }, HEARTBEAT_MS);
57
+ // Never keep the process alive just for telemetry.
58
+ flushTimer.unref?.();
59
+ }
60
+
61
+ /** Record that a browser session became resident. */
62
+ export function sessionStarted(sessionId: string): void {
63
+ if (!ENABLED) return;
64
+ openSessions.add(sessionId);
65
+ pushEvent({ type: 'session_start', sessionId, ts: Date.now() });
66
+ ensureTimer();
67
+ }
68
+
69
+ /** Record that a browser session was torn down (idle | explicit | shutdown). */
70
+ export function sessionEnded(sessionId: string, reason: string): void {
71
+ if (!ENABLED) return;
72
+ openSessions.delete(sessionId);
73
+ pushEvent({ type: 'session_end', sessionId, ts: Date.now(), reason });
74
+ // Flush promptly so the host can finalize this session's billing.
75
+ void flush();
76
+ }
77
+
78
+ async function flush(): Promise<void> {
79
+ if (!ENABLED) return;
80
+ const now = Date.now();
81
+ // Heartbeat every still-open session — proof the browser is still resident.
82
+ for (const id of openSessions) {
83
+ pushEvent({ type: 'heartbeat', sessionId: id, ts: now });
84
+ }
85
+ if (buffer.length === 0) return;
86
+ const events = buffer.splice(0, buffer.length);
87
+ try {
88
+ const controller = new AbortController();
89
+ const timer = setTimeout(() => controller.abort(), POST_TIMEOUT_MS);
90
+ try {
91
+ await fetch(TELEMETRY_URL as string, {
92
+ method: 'POST',
93
+ headers: {
94
+ 'Content-Type': 'application/json',
95
+ Authorization: `Bearer ${TELEMETRY_TOKEN}`,
96
+ },
97
+ body: JSON.stringify({ v: 1, slug: SERVER_SLUG, events }),
98
+ signal: controller.signal,
99
+ });
100
+ } finally {
101
+ clearTimeout(timer);
102
+ }
103
+ } catch {
104
+ // Swallow: telemetry is best-effort and must never affect the server.
105
+ }
106
+ }
107
+
108
+ /** Best-effort final flush on shutdown: end every open session and send. */
109
+ export async function flushOnShutdown(reason = 'shutdown'): Promise<void> {
110
+ if (!ENABLED) return;
111
+ const now = Date.now();
112
+ for (const id of [...openSessions]) {
113
+ openSessions.delete(id);
114
+ pushEvent({ type: 'session_end', sessionId: id, ts: now, reason });
115
+ }
116
+ await flush();
117
+ }
@@ -45,8 +45,8 @@ export function register(server: McpServer, config: AppConfig): void {
45
45
  {{#each form.fields}}
46
46
  {{#unless (eq fieldType "hidden")}}
47
47
  {{#if (eq fieldType "checkbox")}}
48
- if ((input as Record<string, unknown>)['{{jsString name}}'] !== undefined) {
49
- const checked = Boolean((input as Record<string, unknown>)['{{jsString name}}']);
48
+ if ((input as Record<string, unknown>)['{{jsString inputKey}}'] !== undefined) {
49
+ const checked = Boolean((input as Record<string, unknown>)['{{jsString inputKey}}']);
50
50
  const fieldSelector = await resolveSelector(page, ['{{jsString selector.primary}}'{{#each selector.fallbacks}}, '{{jsString this}}'{{/each}}]);
51
51
  if (checked) {
52
52
  await page.check(fieldSelector);
@@ -55,21 +55,23 @@ export function register(server: McpServer, config: AppConfig): void {
55
55
  }
56
56
  }
57
57
  {{else if (eq fieldType "select")}}
58
- if ((input as Record<string, unknown>)['{{jsString name}}'] !== undefined) {
58
+ if ((input as Record<string, unknown>)['{{jsString inputKey}}'] !== undefined) {
59
59
  const fieldSelector = await resolveSelector(page, ['{{jsString selector.primary}}'{{#each selector.fallbacks}}, '{{jsString this}}'{{/each}}]);
60
- await page.selectOption(fieldSelector, String((input as Record<string, unknown>)['{{jsString name}}']));
60
+ // The schema enum is built from option VALUES, and selectOption
61
+ // matches by value, so pass the value through directly.
62
+ await page.selectOption(fieldSelector, String((input as Record<string, unknown>)['{{jsString inputKey}}']));
61
63
  }
62
64
  {{else if (eq fieldType "radio")}}
63
- if ((input as Record<string, unknown>)['{{jsString name}}'] !== undefined) {
65
+ if ((input as Record<string, unknown>)['{{jsString inputKey}}'] !== undefined) {
64
66
  const radioName = '{{jsString name}}';
65
- const radioValue = String((input as Record<string, unknown>)['{{jsString name}}']).replace(/["\\[\]()]/g, '\\$&');
67
+ const radioValue = String((input as Record<string, unknown>)['{{jsString inputKey}}']).replace(/["\\[\]()]/g, '\\$&');
66
68
  const radioNameSel = radioName.replace(/["\\]/g, '\\$&');
67
69
  await page.check(`input[name="${radioNameSel}"][value="${radioValue}"]`);
68
70
  }
69
71
  {{else}}
70
- if ((input as Record<string, unknown>)['{{jsString name}}'] !== undefined) {
72
+ if ((input as Record<string, unknown>)['{{jsString inputKey}}'] !== undefined) {
71
73
  const fieldSelector = await resolveSelector(page, ['{{jsString selector.primary}}'{{#each selector.fallbacks}}, '{{jsString this}}'{{/each}}]);
72
- await page.fill(fieldSelector, String((input as Record<string, unknown>)['{{jsString name}}']));
74
+ await page.fill(fieldSelector, String((input as Record<string, unknown>)['{{jsString inputKey}}']));
73
75
  }
74
76
  {{/if}}
75
77
  {{/unless}}
@@ -1 +1 @@
1
- {"version":3,"file":"template-loader.d.ts","sourceRoot":"","sources":["../../src/emitter/template-loader.ts"],"names":[],"mappings":"AAoBA,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,GAAG,MAAM,CAU9D"}
1
+ {"version":3,"file":"template-loader.d.ts","sourceRoot":"","sources":["../../src/emitter/template-loader.ts"],"names":[],"mappings":"AA8BA,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,GAAG,MAAM,CAU9D"}
@@ -13,6 +13,15 @@ Handlebars.registerHelper('capitalize', function (str) {
13
13
  return str;
14
14
  return str.charAt(0).toUpperCase() + str.slice(1);
15
15
  });
16
+ // Emit a value as a valid JS/TS literal via JSON.stringify. Safe for embedding
17
+ // untrusted spec-derived strings (e.g. OpenAPI security-scheme names) into
18
+ // generated code: JSON.stringify produces a double-quoted literal with all
19
+ // special characters escaped, so the value can never break out of the literal.
20
+ // Registered on the shared Handlebars singleton, so worker templates (which
21
+ // import the same instance) can use it too.
22
+ Handlebars.registerHelper('json', function (value) {
23
+ return JSON.stringify(value === undefined ? null : value);
24
+ });
16
25
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
26
  export function renderTemplate(name, data) {
18
27
  if (!templateCache.has(name)) {
@@ -1 +1 @@
1
- {"version":3,"file":"template-loader.js","sourceRoot":"","sources":["../../src/emitter/template-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,UAAU,MAAM,YAAY,CAAC;AAEpC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC1D,MAAM,YAAY,GAAG,OAAO,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;AAErD,MAAM,aAAa,GAAG,IAAI,GAAG,EAAuC,CAAC;AAErE,UAAU,CAAC,cAAc,CAAC,IAAI,EAAE,UAAU,CAAU,EAAE,CAAU;IAC9D,OAAO,CAAC,KAAK,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC;AAEH,UAAU,CAAC,cAAc,CAAC,YAAY,EAAE,UAAU,GAAW;IAC3D,IAAI,CAAC,GAAG;QAAE,OAAO,GAAG,CAAC;IACrB,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACpD,CAAC,CAAC,CAAC;AAEH,8DAA8D;AAC9D,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,IAAS;IACpD,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7B,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,EAAE,GAAG,IAAI,MAAM,CAAC,CAAC;QAC1D,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,YAAY,GAAG,GAAG,CAAC,EAAE,CAAC;YACjD,MAAM,IAAI,KAAK,CAAC,0BAA0B,IAAI,EAAE,CAAC,CAAC;QACpD,CAAC;QACD,MAAM,GAAG,GAAG,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QAChD,aAAa,CAAC,GAAG,CAAC,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACvE,CAAC;IACD,OAAO,aAAa,CAAC,GAAG,CAAC,IAAI,CAAE,CAAC,IAAI,CAAC,CAAC;AACxC,CAAC"}
1
+ {"version":3,"file":"template-loader.js","sourceRoot":"","sources":["../../src/emitter/template-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,UAAU,MAAM,YAAY,CAAC;AAEpC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC1D,MAAM,YAAY,GAAG,OAAO,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;AAErD,MAAM,aAAa,GAAG,IAAI,GAAG,EAAuC,CAAC;AAErE,UAAU,CAAC,cAAc,CAAC,IAAI,EAAE,UAAU,CAAU,EAAE,CAAU;IAC9D,OAAO,CAAC,KAAK,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC;AAEH,UAAU,CAAC,cAAc,CAAC,YAAY,EAAE,UAAU,GAAW;IAC3D,IAAI,CAAC,GAAG;QAAE,OAAO,GAAG,CAAC;IACrB,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACpD,CAAC,CAAC,CAAC;AAEH,+EAA+E;AAC/E,2EAA2E;AAC3E,2EAA2E;AAC3E,+EAA+E;AAC/E,4EAA4E;AAC5E,4CAA4C;AAC5C,UAAU,CAAC,cAAc,CAAC,MAAM,EAAE,UAAU,KAAc;IACxD,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AAC5D,CAAC,CAAC,CAAC;AAEH,8DAA8D;AAC9D,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,IAAS;IACpD,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7B,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,EAAE,GAAG,IAAI,MAAM,CAAC,CAAC;QAC1D,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,YAAY,GAAG,GAAG,CAAC,EAAE,CAAC;YACjD,MAAM,IAAI,KAAK,CAAC,0BAA0B,IAAI,EAAE,CAAC,CAAC;QACpD,CAAC;QACD,MAAM,GAAG,GAAG,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QAChD,aAAa,CAAC,GAAG,CAAC,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACvE,CAAC;IACD,OAAO,aAAa,CAAC,GAAG,CAAC,IAAI,CAAE,CAAC,IAAI,CAAC,CAAC;AACxC,CAAC"}