@sync.so/mcp-server 0.1.3 → 0.2.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 (261) hide show
  1. package/README.md +261 -86
  2. package/deploy/generation-status/app.html.gz +0 -0
  3. package/deploy/generation-status/release.json +6 -0
  4. package/dist/abort-signals.d.ts +6 -0
  5. package/dist/abort-signals.d.ts.map +1 -0
  6. package/dist/abort-signals.js +23 -0
  7. package/dist/account-features.d.ts +7 -0
  8. package/dist/account-features.d.ts.map +1 -0
  9. package/dist/account-features.js +48 -0
  10. package/dist/app-upload-relay.d.ts +13 -0
  11. package/dist/app-upload-relay.d.ts.map +1 -0
  12. package/dist/app-upload-relay.js +136 -0
  13. package/dist/asset-thumbnail.d.ts +3 -0
  14. package/dist/asset-thumbnail.d.ts.map +1 -0
  15. package/dist/asset-thumbnail.js +2 -0
  16. package/dist/auth/async-context.d.ts +5 -0
  17. package/dist/auth/async-context.d.ts.map +1 -0
  18. package/dist/auth/async-context.js +14 -0
  19. package/dist/auth/bearer-auth.d.ts +16 -0
  20. package/dist/auth/bearer-auth.d.ts.map +1 -0
  21. package/dist/auth/bearer-auth.js +55 -0
  22. package/dist/auth/client-credentials.d.ts +13 -0
  23. package/dist/auth/client-credentials.d.ts.map +1 -0
  24. package/dist/auth/client-credentials.js +67 -0
  25. package/dist/auth/device-auth.d.ts.map +1 -1
  26. package/dist/auth/device-auth.js +3 -2
  27. package/dist/auth/fixture-cleanup.d.ts +4 -0
  28. package/dist/auth/fixture-cleanup.d.ts.map +1 -0
  29. package/dist/auth/fixture-cleanup.js +9 -0
  30. package/dist/auth/index.d.ts +2 -0
  31. package/dist/auth/index.d.ts.map +1 -1
  32. package/dist/auth/index.js +2 -0
  33. package/dist/auth/oauth-diagnostics.d.ts +11 -0
  34. package/dist/auth/oauth-diagnostics.d.ts.map +1 -0
  35. package/dist/auth/oauth-diagnostics.js +78 -0
  36. package/dist/auth/oauth-provider.d.ts +16 -0
  37. package/dist/auth/oauth-provider.d.ts.map +1 -0
  38. package/dist/auth/oauth-provider.js +107 -0
  39. package/dist/auth/resource-metadata.d.ts +2 -0
  40. package/dist/auth/resource-metadata.d.ts.map +1 -0
  41. package/dist/auth/resource-metadata.js +3 -0
  42. package/dist/auth/token-verification.d.ts +11 -0
  43. package/dist/auth/token-verification.d.ts.map +1 -0
  44. package/dist/auth/token-verification.js +99 -0
  45. package/dist/auth/verification-cache.d.ts +8 -0
  46. package/dist/auth/verification-cache.d.ts.map +1 -0
  47. package/dist/auth/verification-cache.js +111 -0
  48. package/dist/chatgpt-analytics.d.ts +22 -0
  49. package/dist/chatgpt-analytics.d.ts.map +1 -0
  50. package/dist/chatgpt-analytics.js +172 -0
  51. package/dist/chatgpt-app.d.ts +15 -0
  52. package/dist/chatgpt-app.d.ts.map +1 -0
  53. package/dist/chatgpt-app.js +133 -0
  54. package/dist/chatgpt-release.d.ts +12 -0
  55. package/dist/chatgpt-release.d.ts.map +1 -0
  56. package/dist/chatgpt-release.js +30 -0
  57. package/dist/cli.js +30 -16
  58. package/dist/client-profile.d.ts +10 -0
  59. package/dist/client-profile.d.ts.map +1 -0
  60. package/dist/client-profile.js +45 -0
  61. package/dist/config.d.ts +17 -0
  62. package/dist/config.d.ts.map +1 -1
  63. package/dist/config.js +52 -0
  64. package/dist/generation-status-html.d.ts +2 -0
  65. package/dist/generation-status-html.d.ts.map +1 -0
  66. package/dist/generation-status-html.js +26 -0
  67. package/dist/generation-status.d.ts +6 -0
  68. package/dist/generation-status.d.ts.map +1 -0
  69. package/dist/generation-status.js +37 -0
  70. package/dist/http-client.d.ts +45 -1
  71. package/dist/http-client.d.ts.map +1 -1
  72. package/dist/http-client.js +171 -34
  73. package/dist/http-server.d.ts +53 -0
  74. package/dist/http-server.d.ts.map +1 -0
  75. package/dist/http-server.js +753 -0
  76. package/dist/http-session.d.ts +18 -0
  77. package/dist/http-session.d.ts.map +1 -0
  78. package/dist/http-session.js +189 -0
  79. package/dist/index.d.ts +4 -3
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +3 -2
  82. package/dist/oauth-response.d.ts +3 -0
  83. package/dist/oauth-response.d.ts.map +1 -0
  84. package/dist/oauth-response.js +30 -0
  85. package/dist/openapi/index.d.ts +2 -2
  86. package/dist/openapi/index.d.ts.map +1 -1
  87. package/dist/openapi/index.js +1 -1
  88. package/dist/openapi/parser.d.ts +2 -0
  89. package/dist/openapi/parser.d.ts.map +1 -1
  90. package/dist/openapi/parser.js +41 -1
  91. package/dist/openapi/types.d.ts +3 -0
  92. package/dist/openapi/types.d.ts.map +1 -1
  93. package/dist/organization-selection.d.ts +61 -0
  94. package/dist/organization-selection.d.ts.map +1 -0
  95. package/dist/organization-selection.js +142 -0
  96. package/dist/rate-limits.d.ts +13 -0
  97. package/dist/rate-limits.d.ts.map +1 -0
  98. package/dist/rate-limits.js +67 -0
  99. package/dist/revision.d.ts +6 -0
  100. package/dist/revision.d.ts.map +1 -0
  101. package/dist/revision.js +17 -0
  102. package/dist/runtime-diagnostics.d.ts +32 -0
  103. package/dist/runtime-diagnostics.d.ts.map +1 -0
  104. package/dist/runtime-diagnostics.js +58 -0
  105. package/dist/server.d.ts +23 -2
  106. package/dist/server.d.ts.map +1 -1
  107. package/dist/server.js +893 -32
  108. package/dist/session-registry.d.ts +54 -0
  109. package/dist/session-registry.d.ts.map +1 -0
  110. package/dist/session-registry.js +202 -0
  111. package/dist/session-telemetry.d.ts +13 -0
  112. package/dist/session-telemetry.d.ts.map +1 -0
  113. package/dist/session-telemetry.js +87 -0
  114. package/dist/spend-confirmation.d.ts +21 -0
  115. package/dist/spend-confirmation.d.ts.map +1 -0
  116. package/dist/spend-confirmation.js +105 -0
  117. package/dist/tool-analytics-context.d.ts +11 -0
  118. package/dist/tool-analytics-context.d.ts.map +1 -0
  119. package/dist/tool-analytics-context.js +8 -0
  120. package/dist/tool-names.d.ts +35 -0
  121. package/dist/tool-names.d.ts.map +1 -0
  122. package/dist/tool-names.js +133 -0
  123. package/dist/tools/account-context.d.ts +5 -0
  124. package/dist/tools/account-context.d.ts.map +1 -0
  125. package/dist/tools/account-context.js +131 -0
  126. package/dist/tools/account-management.d.ts +4 -0
  127. package/dist/tools/account-management.d.ts.map +1 -0
  128. package/dist/tools/account-management.js +111 -0
  129. package/dist/tools/app-tools.d.ts +31 -0
  130. package/dist/tools/app-tools.d.ts.map +1 -0
  131. package/dist/tools/app-tools.js +350 -0
  132. package/dist/tools/asset-analysis.d.ts +4 -0
  133. package/dist/tools/asset-analysis.d.ts.map +1 -0
  134. package/dist/tools/asset-analysis.js +172 -0
  135. package/dist/tools/asset-creators.d.ts +4 -0
  136. package/dist/tools/asset-creators.d.ts.map +1 -0
  137. package/dist/tools/asset-creators.js +38 -0
  138. package/dist/tools/audio-trim.d.ts +6 -0
  139. package/dist/tools/audio-trim.d.ts.map +1 -0
  140. package/dist/tools/audio-trim.js +79 -0
  141. package/dist/tools/batch-results.d.ts +16 -0
  142. package/dist/tools/batch-results.d.ts.map +1 -0
  143. package/dist/tools/batch-results.js +103 -0
  144. package/dist/tools/batches.d.ts +28 -0
  145. package/dist/tools/batches.d.ts.map +1 -0
  146. package/dist/tools/batches.js +293 -0
  147. package/dist/tools/dialogue.d.ts +6 -0
  148. package/dist/tools/dialogue.d.ts.map +1 -0
  149. package/dist/tools/dialogue.js +309 -0
  150. package/dist/tools/estimate-duration.d.ts +35 -0
  151. package/dist/tools/estimate-duration.d.ts.map +1 -0
  152. package/dist/tools/estimate-duration.js +66 -0
  153. package/dist/tools/feedback.d.ts +14 -0
  154. package/dist/tools/feedback.d.ts.map +1 -0
  155. package/dist/tools/feedback.js +72 -0
  156. package/dist/tools/generated-adapters.d.ts +25 -0
  157. package/dist/tools/generated-adapters.d.ts.map +1 -0
  158. package/dist/tools/generated-adapters.js +143 -0
  159. package/dist/tools/generation-context.d.ts +14 -0
  160. package/dist/tools/generation-context.d.ts.map +1 -0
  161. package/dist/tools/generation-context.js +39 -0
  162. package/dist/tools/generation-continuation.d.ts +4 -0
  163. package/dist/tools/generation-continuation.d.ts.map +1 -0
  164. package/dist/tools/generation-continuation.js +124 -0
  165. package/dist/tools/generation-costs.d.ts +6 -0
  166. package/dist/tools/generation-costs.d.ts.map +1 -0
  167. package/dist/tools/generation-costs.js +67 -0
  168. package/dist/tools/generation-output.d.ts +3 -0
  169. package/dist/tools/generation-output.d.ts.map +1 -0
  170. package/dist/tools/generation-output.js +39 -0
  171. package/dist/tools/generator.d.ts +35 -2
  172. package/dist/tools/generator.d.ts.map +1 -1
  173. package/dist/tools/generator.js +127 -9
  174. package/dist/tools/job-status.d.ts +35 -0
  175. package/dist/tools/job-status.d.ts.map +1 -0
  176. package/dist/tools/job-status.js +92 -0
  177. package/dist/tools/job-wait.d.ts +41 -0
  178. package/dist/tools/job-wait.d.ts.map +1 -0
  179. package/dist/tools/job-wait.js +84 -0
  180. package/dist/tools/jobs.d.ts +3 -0
  181. package/dist/tools/jobs.d.ts.map +1 -0
  182. package/dist/tools/jobs.js +210 -0
  183. package/dist/tools/media-upload.d.ts +27 -0
  184. package/dist/tools/media-upload.d.ts.map +1 -0
  185. package/dist/tools/media-upload.js +247 -0
  186. package/dist/tools/model-options.d.ts +36 -0
  187. package/dist/tools/model-options.d.ts.map +1 -0
  188. package/dist/tools/model-options.js +48 -0
  189. package/dist/tools/organization-choice.d.ts +9 -0
  190. package/dist/tools/organization-choice.d.ts.map +1 -0
  191. package/dist/tools/organization-choice.js +136 -0
  192. package/dist/tools/organizations.d.ts +56 -0
  193. package/dist/tools/organizations.d.ts.map +1 -0
  194. package/dist/tools/organizations.js +235 -0
  195. package/dist/tools/output-schemas.d.ts +54 -0
  196. package/dist/tools/output-schemas.d.ts.map +1 -0
  197. package/dist/tools/output-schemas.js +58 -0
  198. package/dist/tools/overrides.d.ts +3 -0
  199. package/dist/tools/overrides.d.ts.map +1 -1
  200. package/dist/tools/overrides.js +113 -15
  201. package/dist/tools/playback-metadata.d.ts +4 -0
  202. package/dist/tools/playback-metadata.d.ts.map +1 -0
  203. package/dist/tools/playback-metadata.js +51 -0
  204. package/dist/tools/project-destination.d.ts +13 -0
  205. package/dist/tools/project-destination.d.ts.map +1 -0
  206. package/dist/tools/project-destination.js +45 -0
  207. package/dist/tools/project-history.d.ts +6 -0
  208. package/dist/tools/project-history.d.ts.map +1 -0
  209. package/dist/tools/project-history.js +45 -0
  210. package/dist/tools/project-intent.d.ts +8 -0
  211. package/dist/tools/project-intent.d.ts.map +1 -0
  212. package/dist/tools/project-intent.js +87 -0
  213. package/dist/tools/running-submissions.d.ts +7 -0
  214. package/dist/tools/running-submissions.d.ts.map +1 -0
  215. package/dist/tools/running-submissions.js +91 -0
  216. package/dist/tools/speech.d.ts +6 -0
  217. package/dist/tools/speech.d.ts.map +1 -0
  218. package/dist/tools/speech.js +116 -0
  219. package/dist/tools/studio-draft.d.ts +7 -0
  220. package/dist/tools/studio-draft.d.ts.map +1 -0
  221. package/dist/tools/studio-draft.js +195 -0
  222. package/dist/tools/studio-view-contract.d.ts +184 -0
  223. package/dist/tools/studio-view-contract.d.ts.map +1 -0
  224. package/dist/tools/studio-view-contract.js +67 -0
  225. package/dist/tools/studio-view.d.ts +6 -0
  226. package/dist/tools/studio-view.d.ts.map +1 -0
  227. package/dist/tools/studio-view.js +126 -0
  228. package/dist/tools/timeline-lipsync.d.ts +316 -0
  229. package/dist/tools/timeline-lipsync.d.ts.map +1 -0
  230. package/dist/tools/timeline-lipsync.js +304 -0
  231. package/dist/tools/translation-preview.d.ts +8 -0
  232. package/dist/tools/translation-preview.d.ts.map +1 -0
  233. package/dist/tools/translation-preview.js +146 -0
  234. package/dist/tools/translation.d.ts +6 -0
  235. package/dist/tools/translation.d.ts.map +1 -0
  236. package/dist/tools/translation.js +131 -0
  237. package/dist/tools/upload-widget.d.ts +13 -0
  238. package/dist/tools/upload-widget.d.ts.map +1 -0
  239. package/dist/tools/upload-widget.js +788 -0
  240. package/dist/toolsets.d.ts +18 -0
  241. package/dist/toolsets.d.ts.map +1 -0
  242. package/dist/toolsets.js +157 -0
  243. package/dist/update-check.d.ts +38 -0
  244. package/dist/update-check.d.ts.map +1 -0
  245. package/dist/update-check.js +150 -0
  246. package/dist/upload-runtime.d.ts +45 -0
  247. package/dist/upload-runtime.d.ts.map +1 -0
  248. package/dist/upload-runtime.js +156 -0
  249. package/dist/upload-source.d.ts +4 -0
  250. package/dist/upload-source.d.ts.map +1 -0
  251. package/dist/upload-source.js +46 -0
  252. package/dist/version.d.ts +2 -0
  253. package/dist/version.d.ts.map +1 -0
  254. package/dist/version.js +3 -0
  255. package/package.json +79 -33
  256. package/dist/openapi/parser.test.d.ts +0 -2
  257. package/dist/openapi/parser.test.d.ts.map +0 -1
  258. package/dist/openapi/parser.test.js +0 -188
  259. package/dist/tools/generator.test.d.ts +0 -2
  260. package/dist/tools/generator.test.d.ts.map +0 -1
  261. package/dist/tools/generator.test.js +0 -178
@@ -0,0 +1,753 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { monitorEventLoopDelay, performance } from 'node:perf_hooks';
3
+ import { getHeapStatistics } from 'node:v8';
4
+ import { InitializeRequestSchema } from '@modelcontextprotocol/core';
5
+ import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
6
+ import { createOAuthMetadata, InvalidRequestError, mcpAuthRouter, OAuthError, } from '@modelcontextprotocol/server-legacy/auth';
7
+ import cors from 'cors';
8
+ import express from 'express';
9
+ import { appUploadHandler, createAppUploadCors } from './app-upload-relay.js';
10
+ import { ASSET_THUMBNAIL_PLACEHOLDER } from './asset-thumbnail.js';
11
+ import { runWithAuth } from './auth/async-context.js';
12
+ import { requireBearerAuth } from './auth/bearer-auth.js';
13
+ import { BASIC_CHALLENGE, CLIENT_AUTH_METHODS, ClientAuthenticationError, mergeBasicClientCredentials, } from './auth/client-credentials.js';
14
+ import { oauthFailureDiagnostics, oauthGrantType, oauthRequestDiagnostics, } from './auth/oauth-diagnostics.js';
15
+ import { createOAuthProvider } from './auth/oauth-provider.js';
16
+ import { resourceMetadataUrl } from './auth/resource-metadata.js';
17
+ import { HttpSession, isSessionControlRequest } from './http-session.js';
18
+ import { readOAuthResponseText } from './oauth-response.js';
19
+ import { createMcpRejectedCredentialLimit, createMcpUserLimit, createOAuthClientLimit, createOAuthRejectedRequestLimit, } from './rate-limits.js';
20
+ import { REVISION } from './revision.js';
21
+ import { HttpRequestMetrics, serializeError } from './runtime-diagnostics.js';
22
+ import { SessionRegistry, sessionOwner } from './session-registry.js';
23
+ import { SessionTelemetry } from './session-telemetry.js';
24
+ import { uploadRuntime } from './upload-runtime.js';
25
+ export { extractBasicClientCredentials, mergeBasicClientCredentials, } from './auth/client-credentials.js';
26
+ // Bound the entire credential exchange, including a stalled response body.
27
+ export const OAUTH_PROXY_TIMEOUT_MS = 10_000;
28
+ const OAUTH_FORM_FIELDS = [
29
+ 'grant_type',
30
+ 'code',
31
+ 'redirect_uri',
32
+ 'client_id',
33
+ 'client_secret',
34
+ 'code_verifier',
35
+ 'refresh_token',
36
+ 'scope',
37
+ 'resource',
38
+ 'token',
39
+ 'token_type_hint',
40
+ ];
41
+ const OPENAI_APPS_CHALLENGE_TOKEN = process.env.OPENAI_APPS_CHALLENGE_TOKEN || 'npwmwee4nxi0N3Rm14jmmsldnkv27qSnU3mY5rFCc5E';
42
+ const DEFAULT_SESSION_IDLE_TTL_MS = 2 * 60_000;
43
+ const DEFAULT_MAX_SESSIONS = 1_000;
44
+ const DEFAULT_SESSION_SWEEP_INTERVAL_MS = 15_000;
45
+ const DEFAULT_SHUTDOWN_GRACE_MS = 10_000;
46
+ const DEFAULT_RUNTIME_TELEMETRY_INTERVAL_MS = 15_000;
47
+ const MAX_REQUEST_ID_LENGTH = 128;
48
+ // Leave room for existing requests, JSON/UI responses and GC. Never evict an
49
+ // active session or replay an operation merely to admit a new connection.
50
+ export function hasSessionHeapCapacity(heap = getHeapStatistics()) {
51
+ return heap.used_heap_size < heap.heap_size_limit * 0.6;
52
+ }
53
+ // Collect reclaimable objects before refusing new sessions. Bound full-GC work
54
+ // during sustained pressure; keep the same headroom and never evict live work.
55
+ export function createSessionHeapAdmission({ readHeap = getHeapStatistics, collect = global.gc, now = Date.now, } = {}) {
56
+ let lastCollection = Number.NEGATIVE_INFINITY;
57
+ return () => {
58
+ if (hasSessionHeapCapacity(readHeap()))
59
+ return true;
60
+ const time = now();
61
+ if (collect && time - lastCollection >= 30_000) {
62
+ lastCollection = time;
63
+ collect();
64
+ return hasSessionHeapCapacity(readHeap());
65
+ }
66
+ return false;
67
+ };
68
+ }
69
+ function positiveInteger(value, fallback) {
70
+ if (!value)
71
+ return fallback;
72
+ const parsed = Number(value);
73
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
74
+ }
75
+ export function getSessionRuntimeConfig(env = process.env) {
76
+ return {
77
+ idleTtlMs: positiveInteger(env.MCP_SESSION_IDLE_TTL_MS, DEFAULT_SESSION_IDLE_TTL_MS),
78
+ maxSessions: positiveInteger(env.MCP_MAX_SESSIONS, DEFAULT_MAX_SESSIONS),
79
+ maxInFlightRequests: positiveInteger(env.MCP_MAX_IN_FLIGHT_REQUESTS, 32),
80
+ sweepIntervalMs: positiveInteger(env.MCP_SESSION_SWEEP_INTERVAL_MS, DEFAULT_SESSION_SWEEP_INTERVAL_MS),
81
+ shutdownGraceMs: positiveInteger(env.MCP_SHUTDOWN_GRACE_MS, DEFAULT_SHUTDOWN_GRACE_MS),
82
+ telemetryIntervalMs: positiveInteger(env.MCP_RUNTIME_TELEMETRY_INTERVAL_MS, DEFAULT_RUNTIME_TELEMETRY_INTERVAL_MS),
83
+ };
84
+ }
85
+ export async function runSessionSweepSafely(sessions, logEvent) {
86
+ try {
87
+ await sessions.sweep();
88
+ }
89
+ catch (error) {
90
+ logEvent('mcp_session_sweep_error', { error: serializeError(error) });
91
+ }
92
+ }
93
+ export function sanitizeDiagnosticUrl(value) {
94
+ const url = new URL(value);
95
+ return `${url.origin}${url.pathname}`;
96
+ }
97
+ export function createRequestId(value) {
98
+ const requestId = value?.toString();
99
+ return requestId && requestId.length <= MAX_REQUEST_ID_LENGTH ? requestId : randomUUID();
100
+ }
101
+ export function closeHttpServerWithGrace(httpServer, graceMs) {
102
+ let closed = false;
103
+ let forced = false;
104
+ let forceTimer;
105
+ const closing = new Promise((resolve, reject) => {
106
+ httpServer.close((error) => {
107
+ closed = true;
108
+ if (forceTimer)
109
+ clearTimeout(forceTimer);
110
+ if (error)
111
+ reject(error);
112
+ else
113
+ resolve({ forced });
114
+ });
115
+ });
116
+ httpServer.closeIdleConnections();
117
+ if (!closed) {
118
+ forceTimer = setTimeout(() => {
119
+ forced = true;
120
+ httpServer.closeAllConnections();
121
+ }, graceMs);
122
+ forceTimer.unref();
123
+ }
124
+ return closing;
125
+ }
126
+ export async function listenWithCleanup(listen, waitForStartup, cleanup) {
127
+ try {
128
+ const server = listen();
129
+ await waitForStartup(server);
130
+ return server;
131
+ }
132
+ catch (error) {
133
+ cleanup();
134
+ throw error;
135
+ }
136
+ }
137
+ export function waitForHttpServerStartup(httpServer, logEvent) {
138
+ return new Promise((resolve, reject) => {
139
+ let startupPending = true;
140
+ httpServer.on('error', (error) => {
141
+ logEvent('http_server_error', { error: serializeError(error) });
142
+ if (startupPending) {
143
+ startupPending = false;
144
+ reject(error);
145
+ }
146
+ });
147
+ httpServer.once('listening', () => {
148
+ if (!startupPending)
149
+ return;
150
+ startupPending = false;
151
+ resolve();
152
+ });
153
+ });
154
+ }
155
+ const TRUSTED_MCP_HOSTS = ['claude.ai', 'claude.com', 'chatgpt.com'];
156
+ const TRUSTED_LOCAL_MCP_ORIGINS = new Set(['http://localhost:3000', 'http://localhost:5173']);
157
+ /**
158
+ * Validate browser origins before an HTTP request reaches the MCP transport.
159
+ * Non-browser MCP clients normally omit Origin, so an absent header is valid.
160
+ */
161
+ export function isAllowedMcpOrigin(origin) {
162
+ if (origin === undefined)
163
+ return true;
164
+ if (TRUSTED_LOCAL_MCP_ORIGINS.has(origin))
165
+ return true;
166
+ try {
167
+ const url = new URL(origin);
168
+ if (url.origin !== origin || url.protocol !== 'https:' || url.port)
169
+ return false;
170
+ return TRUSTED_MCP_HOSTS.some((trustedHost) => url.hostname === trustedHost || url.hostname.endsWith(`.${trustedHost}`));
171
+ }
172
+ catch {
173
+ return false;
174
+ }
175
+ }
176
+ function asRecord(value) {
177
+ return typeof value === 'object' && value !== null ? value : {};
178
+ }
179
+ export function encodeOAuthFormBody(body) {
180
+ const params = new URLSearchParams();
181
+ for (const field of OAUTH_FORM_FIELDS) {
182
+ const value = body[field];
183
+ if (typeof value === 'string' && value !== '') {
184
+ params.set(field, value);
185
+ }
186
+ }
187
+ return params.toString();
188
+ }
189
+ /** Complete each POST with a JSON response, including large HTML resources. */
190
+ export function createHttpMcpTransport(options) {
191
+ return new NodeStreamableHTTPServerTransport({ ...options, enableJsonResponse: true });
192
+ }
193
+ export async function startHttpServer(serverFactory, config) {
194
+ const log = (message) => {
195
+ process.stderr.write(message);
196
+ };
197
+ const logEvent = (event, details = {}) => {
198
+ log(`${JSON.stringify({
199
+ timestamp: new Date().toISOString(),
200
+ event,
201
+ pod: process.env.HOSTNAME,
202
+ revision: REVISION,
203
+ ...details,
204
+ })}\n`);
205
+ };
206
+ const requestMetrics = new HttpRequestMetrics();
207
+ const app = express();
208
+ app.set('trust proxy', 1);
209
+ // Streamable HTTP servers must reject untrusted browser origins. Put this
210
+ // before CORS so invalid preflight requests are rejected too.
211
+ app.use('/mcp', (req, res, next) => {
212
+ if (!isAllowedMcpOrigin(req.headers.origin)) {
213
+ res.status(403).json({ error: 'Origin not allowed' });
214
+ return;
215
+ }
216
+ next();
217
+ });
218
+ const issuerUrl = new URL(process.env.MCP_ISSUER_URL || `http://localhost:${config.port}`);
219
+ const oauthProvider = createOAuthProvider(config.baseUrl, config.oauthAuthorizationUrl);
220
+ if (config.chatgptApp?.uploadStorageOrigin) {
221
+ app.use('/app-upload', createAppUploadCors(config.chatgptApp.uploadOrigins ?? [
222
+ config.chatgptApp.domain,
223
+ 'https://web-sandbox.oaiusercontent.com',
224
+ ]));
225
+ }
226
+ const mcpCors = cors({
227
+ origin: (origin, callback) => callback(null, isAllowedMcpOrigin(origin)),
228
+ credentials: true,
229
+ });
230
+ app.use((req, res, next) => (req.path === '/app-upload' ? next() : mcpCors(req, res, next)));
231
+ // Structured request logging. Aggregate counters are also emitted in runtime heartbeats.
232
+ app.use((req, res, next) => {
233
+ const requestId = createRequestId(req.headers['x-request-id']);
234
+ res.locals.requestId = requestId;
235
+ res.setHeader('x-request-id', requestId);
236
+ const startedAt = performance.now();
237
+ const finishMetrics = requestMetrics.start();
238
+ let recorded = false;
239
+ const record = (aborted) => {
240
+ if (recorded)
241
+ return;
242
+ recorded = true;
243
+ const durationMs = Math.round((performance.now() - startedAt) * 100) / 100;
244
+ finishMetrics({
245
+ statusCode: res.statusCode,
246
+ aborted,
247
+ responseDelivered: res.writableFinished,
248
+ durationMs,
249
+ });
250
+ logEvent('http_request', {
251
+ requestId,
252
+ method: req.method,
253
+ path: req.path,
254
+ statusCode: res.statusCode,
255
+ contentType: res.getHeader('content-type'),
256
+ durationMs,
257
+ aborted,
258
+ });
259
+ };
260
+ res.once('finish', () => record(false));
261
+ res.once('close', () => record(!res.writableFinished));
262
+ next();
263
+ });
264
+ if (config.chatgptApp?.uploadStorageOrigin) {
265
+ // No user bearer token enters the browser. The single-use ticket is issued only
266
+ // after the authenticated assets/upload call has checked the account's limits.
267
+ app.put('/app-upload', appUploadHandler);
268
+ }
269
+ app.get('/asset-placeholder.svg', (_req, res) => {
270
+ res.setHeader('Cache-Control', 'public, max-age=86400');
271
+ res.type('image/svg+xml').send(ASSET_THUMBNAIL_PLACEHOLDER);
272
+ });
273
+ // Health check (unauthenticated). revision and startedAt let the post-deploy smoke
274
+ // check tell the deployed process from an old one that is still serving.
275
+ const startedAt = new Date().toISOString();
276
+ app.get('/health', (_req, res) => {
277
+ res.json({ status: 'ok', revision: REVISION, startedAt });
278
+ });
279
+ app.get('/.well-known/openai-apps-challenge', (_req, res) => {
280
+ res.type('text/plain').send(OPENAI_APPS_CHALLENGE_TOKEN);
281
+ });
282
+ // Favicon — proxy the Sync logo for connector branding
283
+ app.get('/favicon.ico', async (_req, res) => {
284
+ const upstream = await fetch('https://sync.so/favicon.ico');
285
+ if (!upstream.ok) {
286
+ res.status(404).end();
287
+ return;
288
+ }
289
+ res.setHeader('Content-Type', upstream.headers.get('content-type') || 'image/x-icon');
290
+ res.setHeader('Cache-Control', 'public, max-age=86400');
291
+ const buffer = Buffer.from(await upstream.arrayBuffer());
292
+ res.send(buffer);
293
+ });
294
+ // ChatGPT may send client credentials via HTTP Basic Auth. Also, after an MCP
295
+ // server restart, the SDK can recover registered client metadata but not the
296
+ // raw client_secret. Proxy token/revoke requests with the incoming secret and
297
+ // let the Sync API validate the confidential client.
298
+ app.use(['/token', '/revoke'], express.urlencoded({ extended: false }));
299
+ const oauthRejectedRequestLimit = createOAuthRejectedRequestLimit();
300
+ const oauthClientLimit = createOAuthClientLimit();
301
+ const proxyOAuthFormRequest = async (path, req, res) => {
302
+ // Token responses must never be cached, including upstream error responses.
303
+ res.setHeader('Cache-Control', 'no-store');
304
+ res.setHeader('Pragma', 'no-cache');
305
+ const usesAuthorization = req.headers.authorization !== undefined;
306
+ const body = asRecord(req.body);
307
+ const requestDiagnostics = {
308
+ requestId: res.locals.requestId,
309
+ oauthPath: path,
310
+ grantType: oauthGrantType(body.grant_type),
311
+ authorizationHeaderPresent: usesAuthorization,
312
+ ...oauthRequestDiagnostics(body, req.headers.authorization),
313
+ };
314
+ const logOAuthFailure = (source, statusCode, response) => {
315
+ logEvent('oauth_exchange_rejected', {
316
+ ...requestDiagnostics,
317
+ source,
318
+ statusCode,
319
+ ...oauthFailureDiagnostics(response),
320
+ });
321
+ };
322
+ const controller = new AbortController();
323
+ let timedOut = false;
324
+ const cancel = () => controller.abort();
325
+ const timeout = setTimeout(() => {
326
+ timedOut = true;
327
+ cancel();
328
+ }, OAUTH_PROXY_TIMEOUT_MS);
329
+ timeout.unref();
330
+ req.once('aborted', cancel);
331
+ res.once('close', cancel);
332
+ if (req.aborted || res.destroyed)
333
+ cancel();
334
+ try {
335
+ const authorizationCount = req.rawHeaders.filter((header, index) => index % 2 === 0 && header.toLowerCase() === 'authorization').length;
336
+ if (authorizationCount > 1)
337
+ throw new InvalidRequestError('Multiple Authorization headers are not allowed');
338
+ for (const field of OAUTH_FORM_FIELDS) {
339
+ if (Object.hasOwn(body, field) && typeof body[field] !== 'string') {
340
+ throw new InvalidRequestError('OAuth parameters must be single strings');
341
+ }
342
+ }
343
+ const credentials = mergeBasicClientCredentials(body, req.headers.authorization);
344
+ controller.signal.throwIfAborted();
345
+ const upstream = await fetch(`${config.baseUrl.replace(/\/$/, '')}/v2/oauth/${path}`, {
346
+ method: 'POST',
347
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
348
+ body: encodeOAuthFormBody(credentials),
349
+ redirect: 'error',
350
+ signal: controller.signal,
351
+ });
352
+ const responseBody = await readOAuthResponseText(upstream, controller.signal);
353
+ if (controller.signal.aborted || res.destroyed)
354
+ return;
355
+ if (!upstream.ok)
356
+ logOAuthFailure('upstream', upstream.status, responseBody);
357
+ else {
358
+ logEvent('oauth_exchange_succeeded', {
359
+ ...requestDiagnostics,
360
+ source: 'upstream',
361
+ statusCode: upstream.status,
362
+ });
363
+ }
364
+ if (upstream.status === 401 && usesAuthorization) {
365
+ res.setHeader('WWW-Authenticate', BASIC_CHALLENGE);
366
+ }
367
+ const contentType = upstream.headers.get('content-type');
368
+ if (contentType)
369
+ res.setHeader('Content-Type', contentType);
370
+ // Revoking a refresh token can invalidate its entire access-token family.
371
+ if (path === 'revoke' && upstream.ok)
372
+ oauthProvider.clearVerificationCache();
373
+ res.status(upstream.status).send(responseBody);
374
+ }
375
+ catch (error) {
376
+ if (res.destroyed)
377
+ return;
378
+ if (timedOut) {
379
+ logOAuthFailure('timeout', 504, { error: 'temporarily_unavailable' });
380
+ res.status(504).json({
381
+ error: 'temporarily_unavailable',
382
+ error_description: 'OAuth upstream request timed out',
383
+ });
384
+ return;
385
+ }
386
+ if (error instanceof OAuthError) {
387
+ const status = error instanceof ClientAuthenticationError ? error.status : 400;
388
+ logOAuthFailure('local', status, error.toResponseObject());
389
+ if (status === 401)
390
+ res.setHeader('WWW-Authenticate', BASIC_CHALLENGE);
391
+ res.setHeader('Cache-Control', 'no-store');
392
+ res.status(status).json(error.toResponseObject());
393
+ return;
394
+ }
395
+ logEvent('oauth_proxy_error', {
396
+ requestId: res.locals.requestId,
397
+ oauthPath: path,
398
+ error: { message: 'OAuth upstream request failed' },
399
+ });
400
+ if (!res.headersSent) {
401
+ logOAuthFailure('transport', 502, { error: 'server_error' });
402
+ res.status(502).json({ error: 'OAuth upstream request failed' });
403
+ }
404
+ }
405
+ finally {
406
+ clearTimeout(timeout);
407
+ req.off('aborted', cancel);
408
+ res.off('close', cancel);
409
+ }
410
+ };
411
+ app.post('/token', oauthRejectedRequestLimit, oauthClientLimit, (req, res) => {
412
+ void proxyOAuthFormRequest('token', req, res);
413
+ });
414
+ app.post('/revoke', oauthRejectedRequestLimit, oauthClientLimit, (req, res) => {
415
+ void proxyOAuthFormRequest('revoke', req, res);
416
+ });
417
+ // OAuth auth router — handles /.well-known/*, /authorize, /token, /register, /revoke
418
+ const mcpEndpointUrl = new URL('/mcp', issuerUrl);
419
+ const serviceDocumentationUrl = new URL('https://sync.so/docs');
420
+ // The Sync API OAuth backend issues confidential clients and requires a
421
+ // client_secret at token time. The MCP SDK advertises both confidential and
422
+ // public-client auth by default, so override the AS metadata before mounting
423
+ // the SDK router to keep ChatGPT from registering as a public client.
424
+ const confidentialOAuthMetadata = createOAuthMetadata({
425
+ provider: oauthProvider,
426
+ issuerUrl,
427
+ serviceDocumentationUrl,
428
+ });
429
+ confidentialOAuthMetadata.token_endpoint_auth_methods_supported = CLIENT_AUTH_METHODS;
430
+ confidentialOAuthMetadata.revocation_endpoint_auth_methods_supported = CLIENT_AUTH_METHODS;
431
+ app.get('/.well-known/oauth-authorization-server', (_req, res) => {
432
+ res.json(confidentialOAuthMetadata);
433
+ });
434
+ app.use(mcpAuthRouter({
435
+ provider: oauthProvider,
436
+ issuerUrl,
437
+ resourceServerUrl: mcpEndpointUrl,
438
+ serviceDocumentationUrl,
439
+ }));
440
+ // MCP endpoint: requires a valid Bearer token; rate limits are in rate-limits.ts.
441
+ const bearerAuth = requireBearerAuth({
442
+ verifier: oauthProvider,
443
+ resourceMetadataUrl: resourceMetadataUrl(config.port),
444
+ });
445
+ const mcpRejectedCredentialLimit = createMcpRejectedCredentialLimit();
446
+ const mcpUserLimit = createMcpUserLimit();
447
+ const sessionRuntimeConfig = getSessionRuntimeConfig();
448
+ const canAdmitRequest = createSessionHeapAdmission();
449
+ const sessionTelemetry = new SessionTelemetry(logEvent);
450
+ const sessions = new SessionRegistry({
451
+ idleTtlMs: sessionRuntimeConfig.idleTtlMs,
452
+ maxSessions: sessionRuntimeConfig.maxSessions,
453
+ canAdmit: canAdmitRequest,
454
+ onRemove: (sessionId, reason) => {
455
+ sessionTelemetry.close(sessionId, reason === 'closed' ? 'client_delete' : reason);
456
+ },
457
+ onCloseError: (error) => {
458
+ logEvent('mcp_session_close_error', { error: serializeError(error) });
459
+ },
460
+ });
461
+ const eventLoopDelay = monitorEventLoopDelay({ resolution: 20 });
462
+ eventLoopDelay.enable();
463
+ app.get('/status', mcpRejectedCredentialLimit, bearerAuth, mcpUserLimit, (_req, res) => {
464
+ res.setHeader('Cache-Control', 'no-store');
465
+ res.json({
466
+ sessions: sessions.stats(),
467
+ requests: {
468
+ inFlight: sessions.stats().inFlight,
469
+ limit: sessionRuntimeConfig.maxInFlightRequests,
470
+ },
471
+ memory: {
472
+ heapUsedBytes: process.memoryUsage().heapUsed,
473
+ heapLimitBytes: getHeapStatistics().heap_size_limit,
474
+ },
475
+ });
476
+ });
477
+ const logRuntimeTelemetry = () => {
478
+ const memory = process.memoryUsage();
479
+ const resourceUsage = process.resourceUsage();
480
+ const cpuUsage = process.cpuUsage();
481
+ const eventLoopSampleCount = Number(eventLoopDelay.count);
482
+ const eventLoop = eventLoopSampleCount > 0
483
+ ? {
484
+ samples: eventLoopSampleCount,
485
+ minMs: Math.round((eventLoopDelay.min / 1e6) * 100) / 100,
486
+ meanMs: Math.round((eventLoopDelay.mean / 1e6) * 100) / 100,
487
+ p95Ms: Math.round((eventLoopDelay.percentile(95) / 1e6) * 100) / 100,
488
+ p99Ms: Math.round((eventLoopDelay.percentile(99) / 1e6) * 100) / 100,
489
+ maxMs: Math.round((eventLoopDelay.max / 1e6) * 100) / 100,
490
+ }
491
+ : { samples: 0 };
492
+ logEvent('mcp_runtime', {
493
+ pid: process.pid,
494
+ uptimeSeconds: Math.round(process.uptime()),
495
+ sessions: sessions.stats(),
496
+ requests: requestMetrics.snapshot(),
497
+ uploads: uploadRuntime.snapshot(),
498
+ memory: {
499
+ rssBytes: memory.rss,
500
+ heapUsedBytes: memory.heapUsed,
501
+ heapTotalBytes: memory.heapTotal,
502
+ heapLimitBytes: getHeapStatistics().heap_size_limit,
503
+ externalBytes: memory.external,
504
+ arrayBuffersBytes: memory.arrayBuffers,
505
+ },
506
+ cpu: {
507
+ userMicros: cpuUsage.user,
508
+ systemMicros: cpuUsage.system,
509
+ },
510
+ resources: {
511
+ maxRssKb: resourceUsage.maxRSS,
512
+ voluntaryContextSwitches: resourceUsage.voluntaryContextSwitches,
513
+ involuntaryContextSwitches: resourceUsage.involuntaryContextSwitches,
514
+ fsRead: resourceUsage.fsRead,
515
+ fsWrite: resourceUsage.fsWrite,
516
+ },
517
+ eventLoop,
518
+ });
519
+ eventLoopDelay.reset();
520
+ };
521
+ const runtimeTelemetryInterval = setInterval(logRuntimeTelemetry, sessionRuntimeConfig.telemetryIntervalMs);
522
+ runtimeTelemetryInterval.unref();
523
+ let sweepRunning = false;
524
+ const sessionSweepInterval = setInterval(() => {
525
+ if (sweepRunning)
526
+ return;
527
+ sweepRunning = true;
528
+ void runSessionSweepSafely(sessions, logEvent).finally(() => {
529
+ sweepRunning = false;
530
+ });
531
+ }, sessionRuntimeConfig.sweepIntervalMs);
532
+ sessionSweepInterval.unref();
533
+ // JSON body parsing for MCP requests
534
+ app.use('/mcp', express.json());
535
+ let shuttingDown = false;
536
+ app.all('/mcp', mcpRejectedCredentialLimit, bearerAuth, mcpUserLimit, async (req, res) => {
537
+ const token = req.auth?.token;
538
+ if (!token) {
539
+ res.status(401).json({ error: 'Missing auth token' });
540
+ return;
541
+ }
542
+ const owner = sessionOwner(req.auth);
543
+ if (!owner) {
544
+ res.status(403).json({ error: 'Verified session identity required' });
545
+ return;
546
+ }
547
+ if (shuttingDown) {
548
+ res.setHeader('Retry-After', '10');
549
+ res.status(503).json({ error: 'MCP server is shutting down' });
550
+ return;
551
+ }
552
+ let reservation;
553
+ let lease;
554
+ let initializedSessionId;
555
+ try {
556
+ const suppliedId = req.headers['mcp-session-id'];
557
+ if (suppliedId !== undefined && typeof suppliedId !== 'string') {
558
+ res.status(400).json({ error: 'Invalid session header' });
559
+ return;
560
+ }
561
+ let sessionId = suppliedId;
562
+ let session;
563
+ const rpcMessages = (Array.isArray(req.body) ? req.body : [req.body]).map(asRecord);
564
+ if (sessionId) {
565
+ lease = sessions.acquire(sessionId, owner);
566
+ if (!lease) {
567
+ res.status(404).json({ error: 'Session not found' });
568
+ return;
569
+ }
570
+ session = lease.transport;
571
+ }
572
+ else {
573
+ if (req.method !== 'POST') {
574
+ res.setHeader('Allow', 'POST, DELETE');
575
+ res.status(405).end();
576
+ return;
577
+ }
578
+ const init = InitializeRequestSchema.safeParse(req.body);
579
+ if (!init.success) {
580
+ res.status(400).json({ error: 'Initialize an MCP session before sending requests' });
581
+ return;
582
+ }
583
+ reservation = sessions.reserve();
584
+ if (!reservation) {
585
+ res.setHeader('Retry-After', '60');
586
+ res.status(503).json({ error: 'MCP session capacity reached' });
587
+ return;
588
+ }
589
+ const id = randomUUID();
590
+ sessionId = id;
591
+ initializedSessionId = id;
592
+ session = new HttpSession(init.data.params.clientInfo.name.slice(0, 160), serverFactory.createServer, (version) => sessionTelemetry.protocol(id, version), (details) => logEvent('mcp_rpc_error', {
593
+ ...details,
594
+ sessionRef: sessionTelemetry.reference(id),
595
+ }));
596
+ lease = reservation.commit(id, session, owner);
597
+ reservation = undefined;
598
+ sessionTelemetry.start(id, session.clientName, init.data.params.protocolVersion);
599
+ res.setHeader('Mcp-Session-Id', id);
600
+ logEvent('mcp_session_created', {
601
+ sessionRef: sessionTelemetry.reference(id),
602
+ requestId: res.locals.requestId,
603
+ });
604
+ }
605
+ const correlation = initializedSessionId
606
+ ? { sessionRef: sessionTelemetry.reference(sessionId) }
607
+ : sessionTelemetry.request(sessionId, rpcMessages.flatMap((message) => typeof message.method === 'string' ? [message.method] : []));
608
+ if (req.method === 'DELETE') {
609
+ await sessions.remove(sessionId, 'closed');
610
+ res.status(200).end();
611
+ return;
612
+ }
613
+ // Let cancellation and lifecycle notifications through under pressure.
614
+ if (req.method === 'POST' && !isSessionControlRequest(req.body)) {
615
+ const refusal = sessions.stats().inFlight > sessionRuntimeConfig.maxInFlightRequests
616
+ ? 'capacity'
617
+ : !canAdmitRequest()
618
+ ? 'memory'
619
+ : undefined;
620
+ if (refusal) {
621
+ sessions.recordRejection(refusal);
622
+ if (initializedSessionId) {
623
+ await sessions.remove(initializedSessionId, 'initialization_failed');
624
+ res.removeHeader('Mcp-Session-Id');
625
+ }
626
+ res.setHeader('Retry-After', '60');
627
+ res.status(503).json({ error: 'MCP request capacity reached' });
628
+ return;
629
+ }
630
+ }
631
+ for (const rpcBody of rpcMessages) {
632
+ if (typeof rpcBody.method !== 'string')
633
+ continue;
634
+ const rpcParams = asRecord(rpcBody.params);
635
+ logEvent('mcp_rpc_request', {
636
+ requestId: res.locals.requestId,
637
+ method: rpcBody.method.slice(0, 80),
638
+ ...correlation,
639
+ ...(rpcBody.method === 'tools/call'
640
+ ? {
641
+ tool: typeof rpcParams.name === 'string' && /^[a-zA-Z0-9_-]{1,80}$/.test(rpcParams.name)
642
+ ? rpcParams.name
643
+ : '<other tool>',
644
+ }
645
+ : {}),
646
+ });
647
+ }
648
+ await runWithAuth(token, session.clientName, () => session.handleRequest(req, res, !!initializedSessionId), JSON.stringify([owner.sub, owner.clientId, owner.organizationId]));
649
+ if (initializedSessionId && res.statusCode >= 400)
650
+ await sessions.remove(initializedSessionId, 'initialization_failed');
651
+ }
652
+ catch (error) {
653
+ if (initializedSessionId)
654
+ await sessions.remove(initializedSessionId, 'initialization_failed');
655
+ logEvent('mcp_handler_error', {
656
+ requestId: res.locals.requestId,
657
+ error: serializeError(error),
658
+ });
659
+ if (!res.headersSent)
660
+ res.status(500).json({ error: 'Internal server error' });
661
+ }
662
+ finally {
663
+ lease?.release();
664
+ reservation?.release();
665
+ }
666
+ });
667
+ const onUncaughtException = (error, origin) => {
668
+ logEvent('process_uncaught_exception', { origin, error: serializeError(error) });
669
+ };
670
+ const onWarning = (warning) => {
671
+ logEvent('process_warning', { error: serializeError(warning) });
672
+ };
673
+ const onExit = (code) => {
674
+ logEvent('process_exit', {
675
+ code,
676
+ uptimeSeconds: Math.round(process.uptime()),
677
+ sessions: sessions.stats(),
678
+ requests: requestMetrics.snapshot(),
679
+ });
680
+ };
681
+ process.on('uncaughtExceptionMonitor', onUncaughtException);
682
+ process.on('warning', onWarning);
683
+ process.once('exit', onExit);
684
+ logEvent('process_start', {
685
+ pid: process.pid,
686
+ nodeVersion: process.version,
687
+ sessionConfig: sessionRuntimeConfig,
688
+ });
689
+ const cleanupRuntimeResources = () => {
690
+ clearInterval(sessionSweepInterval);
691
+ clearInterval(runtimeTelemetryInterval);
692
+ eventLoopDelay.disable();
693
+ process.off('uncaughtExceptionMonitor', onUncaughtException);
694
+ process.off('warning', onWarning);
695
+ process.off('exit', onExit);
696
+ };
697
+ const httpServer = await listenWithCleanup(() => app.listen(config.port), async (server) => {
698
+ server.on('close', () => {
699
+ logEvent('http_server_closed');
700
+ });
701
+ await waitForHttpServerStartup(server, logEvent);
702
+ }, cleanupRuntimeResources);
703
+ logEvent('server_listening', {
704
+ port: config.port,
705
+ issuer: issuerUrl.origin,
706
+ apiBaseUrl: sanitizeDiagnosticUrl(config.baseUrl),
707
+ registeredToolCount: serverFactory.toolCount,
708
+ });
709
+ logRuntimeTelemetry();
710
+ const shutdown = async (reason) => {
711
+ if (shuttingDown)
712
+ return;
713
+ shuttingDown = true;
714
+ logEvent('shutdown_started', {
715
+ reason,
716
+ sessions: sessions.stats(),
717
+ requests: requestMetrics.snapshot(),
718
+ });
719
+ logRuntimeTelemetry();
720
+ clearInterval(sessionSweepInterval);
721
+ clearInterval(runtimeTelemetryInterval);
722
+ eventLoopDelay.disable();
723
+ // Stop admission first, then let requests and initializations already in progress drain.
724
+ const httpServerClosing = closeHttpServerWithGrace(httpServer, sessionRuntimeConfig.shutdownGraceMs);
725
+ const deadline = Date.now() + sessionRuntimeConfig.shutdownGraceMs;
726
+ let runtimeStats = sessions.stats();
727
+ while ((runtimeStats.pending > 0 || runtimeStats.inFlight > 0) && Date.now() < deadline) {
728
+ await new Promise((resolve) => setTimeout(resolve, 25));
729
+ runtimeStats = sessions.stats();
730
+ }
731
+ if (runtimeStats.pending > 0 || runtimeStats.inFlight > 0) {
732
+ logEvent('shutdown_grace_expired', { sessions: runtimeStats });
733
+ }
734
+ // Freeze reservations before taking final snapshots so late initialization cannot escape cleanup.
735
+ sessions.stopAccepting();
736
+ await sessions.closeAll();
737
+ const httpServerClose = await httpServerClosing;
738
+ logEvent('shutdown_completed', {
739
+ reason,
740
+ forcedHttpConnectionsClosed: httpServerClose.forced,
741
+ sessions: sessions.stats(),
742
+ requests: requestMetrics.snapshot(),
743
+ });
744
+ };
745
+ const requestShutdown = (reason) => {
746
+ void shutdown(reason).catch((error) => {
747
+ logEvent('shutdown_error', { reason, error: serializeError(error) });
748
+ process.exitCode = 1;
749
+ });
750
+ };
751
+ process.once('SIGTERM', () => requestShutdown('SIGTERM'));
752
+ process.once('SIGINT', () => requestShutdown('SIGINT'));
753
+ }