@j0hanz/filesystem-mcp 1.19.0 → 2.0.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 (301) hide show
  1. package/README.md +333 -838
  2. package/dist/cli-help.d.ts +3 -0
  3. package/dist/cli-help.d.ts.map +1 -0
  4. package/dist/cli-help.js +169 -0
  5. package/dist/cli-help.js.map +1 -0
  6. package/dist/cli.d.ts +28 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +168 -123
  9. package/dist/cli.js.map +1 -0
  10. package/dist/core/concurrency.d.ts +70 -0
  11. package/dist/core/concurrency.d.ts.map +1 -0
  12. package/dist/core/concurrency.js +173 -0
  13. package/dist/core/concurrency.js.map +1 -0
  14. package/dist/core/config.d.ts +19 -0
  15. package/dist/core/config.d.ts.map +1 -0
  16. package/dist/core/config.js +11 -0
  17. package/dist/core/config.js.map +1 -0
  18. package/dist/core/cursor.d.ts +23 -0
  19. package/dist/core/cursor.d.ts.map +1 -0
  20. package/dist/core/cursor.js +62 -0
  21. package/dist/core/cursor.js.map +1 -0
  22. package/dist/core/errors.d.ts +96 -0
  23. package/dist/core/errors.d.ts.map +1 -0
  24. package/dist/core/errors.js +356 -0
  25. package/dist/core/errors.js.map +1 -0
  26. package/dist/core/file-uri.d.ts +24 -0
  27. package/dist/core/file-uri.d.ts.map +1 -0
  28. package/dist/core/file-uri.js +52 -0
  29. package/dist/core/file-uri.js.map +1 -0
  30. package/dist/core/fmt.d.ts +38 -0
  31. package/dist/core/fmt.d.ts.map +1 -0
  32. package/dist/core/fmt.js +109 -0
  33. package/dist/core/fmt.js.map +1 -0
  34. package/dist/core/fs.d.ts +108 -0
  35. package/dist/core/fs.d.ts.map +1 -0
  36. package/dist/core/fs.js +243 -0
  37. package/dist/core/fs.js.map +1 -0
  38. package/dist/core/glob.d.ts +41 -0
  39. package/dist/core/glob.d.ts.map +1 -0
  40. package/dist/core/glob.js +399 -0
  41. package/dist/core/glob.js.map +1 -0
  42. package/dist/core/input-required.d.ts +147 -0
  43. package/dist/core/input-required.d.ts.map +1 -0
  44. package/dist/core/input-required.js +261 -0
  45. package/dist/core/input-required.js.map +1 -0
  46. package/dist/core/mime.d.ts +15 -0
  47. package/dist/core/mime.d.ts.map +1 -0
  48. package/dist/core/mime.js +188 -0
  49. package/dist/core/mime.js.map +1 -0
  50. package/dist/core/observability.d.ts +10 -0
  51. package/dist/core/observability.d.ts.map +1 -0
  52. package/dist/core/observability.js +76 -0
  53. package/dist/core/observability.js.map +1 -0
  54. package/dist/core/page-store.d.ts +41 -0
  55. package/dist/core/page-store.d.ts.map +1 -0
  56. package/dist/core/page-store.js +74 -0
  57. package/dist/core/page-store.js.map +1 -0
  58. package/dist/core/path-completer.d.ts +10 -0
  59. package/dist/core/path-completer.d.ts.map +1 -0
  60. package/dist/core/path-completer.js +227 -0
  61. package/dist/core/path-completer.js.map +1 -0
  62. package/dist/core/path-discovery.d.ts +8 -0
  63. package/dist/core/path-discovery.d.ts.map +1 -0
  64. package/dist/core/path-discovery.js +105 -0
  65. package/dist/core/path-discovery.js.map +1 -0
  66. package/dist/core/path-utils.d.ts +9 -0
  67. package/dist/core/path-utils.d.ts.map +1 -0
  68. package/dist/core/path-utils.js +121 -0
  69. package/dist/core/path-utils.js.map +1 -0
  70. package/dist/core/path.d.ts +148 -0
  71. package/dist/core/path.d.ts.map +1 -0
  72. package/dist/core/path.js +674 -0
  73. package/dist/core/path.js.map +1 -0
  74. package/dist/core/primitives.d.ts +23 -0
  75. package/dist/core/primitives.d.ts.map +1 -0
  76. package/dist/core/primitives.js +54 -0
  77. package/dist/core/primitives.js.map +1 -0
  78. package/dist/core/read.d.ts +74 -0
  79. package/dist/core/read.d.ts.map +1 -0
  80. package/dist/core/read.js +562 -0
  81. package/dist/core/read.js.map +1 -0
  82. package/dist/core/schema.d.ts +139 -0
  83. package/dist/core/schema.d.ts.map +1 -0
  84. package/dist/core/schema.js +345 -0
  85. package/dist/core/schema.js.map +1 -0
  86. package/dist/core/search.d.ts +100 -0
  87. package/dist/core/search.d.ts.map +1 -0
  88. package/dist/core/search.js +248 -0
  89. package/dist/core/search.js.map +1 -0
  90. package/dist/core/sensitive.d.ts +6 -0
  91. package/dist/core/sensitive.d.ts.map +1 -0
  92. package/dist/core/sensitive.js +142 -0
  93. package/dist/core/sensitive.js.map +1 -0
  94. package/dist/core/store.d.ts +57 -0
  95. package/dist/core/store.d.ts.map +1 -0
  96. package/dist/core/store.js +162 -0
  97. package/dist/core/store.js.map +1 -0
  98. package/dist/core/util.d.ts +26 -0
  99. package/dist/core/util.d.ts.map +1 -0
  100. package/dist/core/util.js +66 -0
  101. package/dist/core/util.js.map +1 -0
  102. package/dist/core/watcher-registry.d.ts +68 -0
  103. package/dist/core/watcher-registry.d.ts.map +1 -0
  104. package/dist/core/watcher-registry.js +312 -0
  105. package/dist/core/watcher-registry.js.map +1 -0
  106. package/dist/http-policy.d.ts +121 -0
  107. package/dist/http-policy.d.ts.map +1 -0
  108. package/dist/http-policy.js +340 -0
  109. package/dist/http-policy.js.map +1 -0
  110. package/dist/index.d.ts +1 -0
  111. package/dist/index.d.ts.map +1 -0
  112. package/dist/index.js +77 -41
  113. package/dist/index.js.map +1 -0
  114. package/dist/instructions.d.ts +12 -0
  115. package/dist/instructions.d.ts.map +1 -0
  116. package/dist/instructions.js +71 -0
  117. package/dist/instructions.js.map +1 -0
  118. package/dist/prompts.d.ts +8 -6
  119. package/dist/prompts.d.ts.map +1 -0
  120. package/dist/prompts.js +74 -168
  121. package/dist/prompts.js.map +1 -0
  122. package/dist/resources.d.ts +73 -9
  123. package/dist/resources.d.ts.map +1 -0
  124. package/dist/resources.js +392 -163
  125. package/dist/resources.js.map +1 -0
  126. package/dist/server.d.ts +62 -1
  127. package/dist/server.d.ts.map +1 -0
  128. package/dist/server.js +139 -1
  129. package/dist/server.js.map +1 -0
  130. package/dist/tools/batch.d.ts +89 -0
  131. package/dist/tools/batch.d.ts.map +1 -0
  132. package/dist/tools/batch.js +157 -0
  133. package/dist/tools/batch.js.map +1 -0
  134. package/dist/tools/create.d.ts +2 -0
  135. package/dist/tools/create.d.ts.map +1 -0
  136. package/dist/tools/create.js +142 -0
  137. package/dist/tools/create.js.map +1 -0
  138. package/dist/tools/define.d.ts +110 -0
  139. package/dist/tools/define.d.ts.map +1 -0
  140. package/dist/tools/define.js +394 -0
  141. package/dist/tools/define.js.map +1 -0
  142. package/dist/tools/delete-file.d.ts +2 -4
  143. package/dist/tools/delete-file.d.ts.map +1 -0
  144. package/dist/tools/delete-file.js +330 -87
  145. package/dist/tools/delete-file.js.map +1 -0
  146. package/dist/tools/diff.d.ts +2 -0
  147. package/dist/tools/diff.d.ts.map +1 -0
  148. package/dist/tools/diff.js +94 -0
  149. package/dist/tools/diff.js.map +1 -0
  150. package/dist/tools/edit.d.ts +2 -0
  151. package/dist/tools/edit.d.ts.map +1 -0
  152. package/dist/tools/edit.js +442 -0
  153. package/dist/tools/edit.js.map +1 -0
  154. package/dist/tools/index.d.ts +26 -0
  155. package/dist/tools/index.d.ts.map +1 -0
  156. package/dist/tools/index.js +52 -0
  157. package/dist/tools/index.js.map +1 -0
  158. package/dist/tools/list.d.ts +2 -0
  159. package/dist/tools/list.d.ts.map +1 -0
  160. package/dist/tools/list.js +291 -0
  161. package/dist/tools/list.js.map +1 -0
  162. package/dist/tools/move.d.ts +2 -0
  163. package/dist/tools/move.d.ts.map +1 -0
  164. package/dist/tools/move.js +321 -0
  165. package/dist/tools/move.js.map +1 -0
  166. package/dist/tools/patch.d.ts +2 -0
  167. package/dist/tools/patch.d.ts.map +1 -0
  168. package/dist/tools/patch.js +155 -0
  169. package/dist/tools/patch.js.map +1 -0
  170. package/dist/tools/progress.d.ts +52 -0
  171. package/dist/tools/progress.d.ts.map +1 -0
  172. package/dist/tools/progress.js +172 -0
  173. package/dist/tools/progress.js.map +1 -0
  174. package/dist/tools/read.d.ts +2 -4
  175. package/dist/tools/read.d.ts.map +1 -0
  176. package/dist/tools/read.js +398 -152
  177. package/dist/tools/read.js.map +1 -0
  178. package/dist/tools/replace-in-files.d.ts +2 -4
  179. package/dist/tools/replace-in-files.d.ts.map +1 -0
  180. package/dist/tools/replace-in-files.js +435 -264
  181. package/dist/tools/replace-in-files.js.map +1 -0
  182. package/dist/tools/roots.d.ts +2 -4
  183. package/dist/tools/roots.d.ts.map +1 -0
  184. package/dist/tools/roots.js +29 -52
  185. package/dist/tools/roots.js.map +1 -0
  186. package/dist/tools/search-content.d.ts +2 -4
  187. package/dist/tools/search-content.d.ts.map +1 -0
  188. package/dist/tools/search-content.js +262 -228
  189. package/dist/tools/search-content.js.map +1 -0
  190. package/dist/tools/search-files.d.ts +2 -4
  191. package/dist/tools/search-files.d.ts.map +1 -0
  192. package/dist/tools/search-files.js +162 -161
  193. package/dist/tools/search-files.js.map +1 -0
  194. package/dist/tools/stat.d.ts +2 -4
  195. package/dist/tools/stat.d.ts.map +1 -0
  196. package/dist/tools/stat.js +202 -62
  197. package/dist/tools/stat.js.map +1 -0
  198. package/dist/transport/http.d.ts +5 -0
  199. package/dist/transport/http.d.ts.map +1 -0
  200. package/dist/transport/http.js +276 -0
  201. package/dist/transport/http.js.map +1 -0
  202. package/dist/transport/shared.d.ts +45 -0
  203. package/dist/transport/shared.d.ts.map +1 -0
  204. package/dist/transport/shared.js +65 -0
  205. package/dist/transport/shared.js.map +1 -0
  206. package/dist/transport/stdio.d.ts +32 -0
  207. package/dist/transport/stdio.d.ts.map +1 -0
  208. package/dist/transport/stdio.js +279 -0
  209. package/dist/transport/stdio.js.map +1 -0
  210. package/dist/transport.d.ts +5 -0
  211. package/dist/transport.d.ts.map +1 -0
  212. package/dist/transport.js +4 -0
  213. package/dist/transport.js.map +1 -0
  214. package/package.json +37 -46
  215. package/dist/assets/logo.svg +0 -35
  216. package/dist/completions.d.ts +0 -2
  217. package/dist/completions.js +0 -572
  218. package/dist/config.d.ts +0 -119
  219. package/dist/config.js +0 -31
  220. package/dist/lib/abort.d.ts +0 -7
  221. package/dist/lib/abort.js +0 -81
  222. package/dist/lib/constants.d.ts +0 -42
  223. package/dist/lib/constants.js +0 -313
  224. package/dist/lib/errors.d.ts +0 -28
  225. package/dist/lib/errors.js +0 -301
  226. package/dist/lib/file-operations/core.d.ts +0 -53
  227. package/dist/lib/file-operations/core.js +0 -144
  228. package/dist/lib/file-operations/metadata.d.ts +0 -77
  229. package/dist/lib/file-operations/metadata.js +0 -878
  230. package/dist/lib/file-operations/search.d.ts +0 -46
  231. package/dist/lib/file-operations/search.js +0 -1194
  232. package/dist/lib/file-operations/traversal.d.ts +0 -40
  233. package/dist/lib/file-operations/traversal.js +0 -310
  234. package/dist/lib/fs-helpers.d.ts +0 -46
  235. package/dist/lib/fs-helpers.js +0 -606
  236. package/dist/lib/globs.d.ts +0 -2
  237. package/dist/lib/globs.js +0 -19
  238. package/dist/lib/logger.d.ts +0 -27
  239. package/dist/lib/logger.js +0 -91
  240. package/dist/lib/observability.d.ts +0 -33
  241. package/dist/lib/observability.js +0 -373
  242. package/dist/lib/paths.d.ts +0 -35
  243. package/dist/lib/paths.js +0 -634
  244. package/dist/lib/resource-store.d.ts +0 -28
  245. package/dist/lib/resource-store.js +0 -195
  246. package/dist/lib/utils.d.ts +0 -7
  247. package/dist/lib/utils.js +0 -37
  248. package/dist/lib/zod-codecs.d.ts +0 -2
  249. package/dist/lib/zod-codecs.js +0 -18
  250. package/dist/pkg-info.d.ts +0 -7
  251. package/dist/pkg-info.js +0 -9
  252. package/dist/resources/generated-instructions.d.ts +0 -1
  253. package/dist/resources/generated-instructions.js +0 -105
  254. package/dist/resources/tool-catalog.d.ts +0 -2
  255. package/dist/resources/tool-catalog.js +0 -78
  256. package/dist/resources/tool-info.d.ts +0 -9
  257. package/dist/resources/tool-info.js +0 -206
  258. package/dist/resources/workflows.d.ts +0 -1
  259. package/dist/resources/workflows.js +0 -72
  260. package/dist/schemas.d.ts +0 -505
  261. package/dist/schemas.js +0 -676
  262. package/dist/server/bootstrap.d.ts +0 -6
  263. package/dist/server/bootstrap.js +0 -563
  264. package/dist/server/event-store.d.ts +0 -18
  265. package/dist/server/event-store.js +0 -71
  266. package/dist/server/roots-manager.d.ts +0 -28
  267. package/dist/server/roots-manager.js +0 -227
  268. package/dist/server/task-store.d.ts +0 -10
  269. package/dist/server/task-store.js +0 -91
  270. package/dist/tools/apply-patch.d.ts +0 -4
  271. package/dist/tools/apply-patch.js +0 -235
  272. package/dist/tools/calculate-hash.d.ts +0 -4
  273. package/dist/tools/calculate-hash.js +0 -187
  274. package/dist/tools/contract.d.ts +0 -50
  275. package/dist/tools/contract.js +0 -1
  276. package/dist/tools/create-directory.d.ts +0 -4
  277. package/dist/tools/create-directory.js +0 -68
  278. package/dist/tools/diff-files.d.ts +0 -4
  279. package/dist/tools/diff-files.js +0 -136
  280. package/dist/tools/edit-file.d.ts +0 -4
  281. package/dist/tools/edit-file.js +0 -256
  282. package/dist/tools/icons.d.ts +0 -15
  283. package/dist/tools/icons.js +0 -24
  284. package/dist/tools/list-directory.d.ts +0 -4
  285. package/dist/tools/list-directory.js +0 -218
  286. package/dist/tools/move-file.d.ts +0 -4
  287. package/dist/tools/move-file.js +0 -178
  288. package/dist/tools/read-multiple.d.ts +0 -4
  289. package/dist/tools/read-multiple.js +0 -207
  290. package/dist/tools/shared.d.ts +0 -172
  291. package/dist/tools/shared.js +0 -599
  292. package/dist/tools/stat-many.d.ts +0 -4
  293. package/dist/tools/stat-many.js +0 -104
  294. package/dist/tools/task-support.d.ts +0 -29
  295. package/dist/tools/task-support.js +0 -472
  296. package/dist/tools/tree.d.ts +0 -4
  297. package/dist/tools/tree.js +0 -102
  298. package/dist/tools/write-file.d.ts +0 -4
  299. package/dist/tools/write-file.js +0 -60
  300. package/dist/tools.d.ts +0 -5
  301. package/dist/tools.js +0 -54
@@ -0,0 +1,340 @@
1
+ import { getOAuthProtectedResourceMetadataUrl } from '@modelcontextprotocol/express';
2
+ import { JSONRPC_VERSION, localhostAllowedHostnames } from '@modelcontextprotocol/server';
3
+ import { createHash, timingSafeEqual } from 'node:crypto';
4
+ import { ErrorCode, FsError } from './core/errors.js';
5
+ import { Logger } from './core/observability.js';
6
+ import { splitCsvList } from './core/util.js';
7
+ const MAX_BEARER_TOKEN_LENGTH = 4096;
8
+ /**
9
+ * JSON-RPC's server-defined error range. No SDK enum maps to "Unauthorized" or
10
+ * "Rate limit exceeded", both of which this module refuses before any handler
11
+ * has parsed a request.
12
+ */
13
+ const JSONRPC_SERVER_ERROR = -32000;
14
+ /**
15
+ * The one JSON-RPC error envelope every HTTP refusal goes out in — this
16
+ * module's pre-handler rejections and `transport.ts`'s. It lives here because
17
+ * `transport.ts` already imports this module and not the other way round; a
18
+ * second hand-built literal is how the two drift apart.
19
+ *
20
+ * `headers` carries what a particular refusal owes the client on top of the
21
+ * JSON content type — `WWW-Authenticate` on a 401, `Retry-After` on a 429.
22
+ * Goes out through `res.json` so every refusal keeps the `charset=utf-8` and
23
+ * `Content-Length` a hand-rolled `writeHead`/`end` pair drops.
24
+ */
25
+ export function sendJsonRpcError(res, status, code, message, id = null, headers = {}) {
26
+ res.status(status).set(headers).json({ jsonrpc: JSONRPC_VERSION, id, error: { code, message } });
27
+ }
28
+ const LOCALHOST_ORIGIN_RE = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/u;
29
+ /**
30
+ * Pure HTTP auth and binding policy. Holds no state; all functions are
31
+ * directly testable without spinning up a server.
32
+ */
33
+ export function isLoopbackHttpHost(host) {
34
+ const normalizedHost = host.trim().toLowerCase();
35
+ return (normalizedHost === '127.0.0.1' ||
36
+ normalizedHost === 'localhost' ||
37
+ normalizedHost === '[::1]' ||
38
+ normalizedHost === '::1');
39
+ }
40
+ export function isAllowedLocalhostOrigin(origin) {
41
+ return LOCALHOST_ORIGIN_RE.test(origin);
42
+ }
43
+ /** A bind to every interface. Clients never send `Host: 0.0.0.0`, so this bind
44
+ * cannot derive its allowed-host set from itself. */
45
+ function isWildcardHttpHost(host) {
46
+ return host === '0.0.0.0' || host === '::';
47
+ }
48
+ function originHostname(origin) {
49
+ try {
50
+ return new URL(origin).hostname;
51
+ }
52
+ catch {
53
+ return undefined;
54
+ }
55
+ }
56
+ /**
57
+ * True if `origin` (a raw `Origin` request header) is allowed given the
58
+ * env-derived `allowedHostnames` set (hostname-form, no scheme/port). Localhost
59
+ * origins are always accepted via {@link isAllowedLocalhostOrigin}; a remote
60
+ * origin is accepted iff its parsed hostname is in the set. Both the SDK app's
61
+ * `allowedOrigins` and this OPTIONS-handler check consume hostname-form, so a
62
+ * remote origin allowed via `FS_ALLOWED_ORIGINS` is reflected
63
+ * end-to-end in `Access-Control-Allow-Origin`.
64
+ */
65
+ export function isOriginAllowed(origin, allowedHostnames) {
66
+ if (isAllowedLocalhostOrigin(origin))
67
+ return true;
68
+ const host = originHostname(origin);
69
+ return host !== undefined && allowedHostnames.includes(host);
70
+ }
71
+ export function validateBearerAuthorization(apiKey, authHeader) {
72
+ const bearerPrefix = 'Bearer ';
73
+ if (typeof authHeader !== 'string' || !authHeader.startsWith(bearerPrefix)) {
74
+ return false;
75
+ }
76
+ const userKey = authHeader.slice(bearerPrefix.length).trim();
77
+ const normalizedApiKey = apiKey.trim();
78
+ if (userKey.length > MAX_BEARER_TOKEN_LENGTH || normalizedApiKey.length === 0) {
79
+ return false;
80
+ }
81
+ // Pure: hash per call. createHash is negligible next to the timingSafeEqual
82
+ // already done per request, and avoiding module-level cache state keeps this
83
+ // testable without post-import env-mutation footguns.
84
+ const expectedHash = createHash('sha256').update(normalizedApiKey).digest();
85
+ const actualHash = createHash('sha256').update(userKey).digest();
86
+ return timingSafeEqual(expectedHash, actualHash);
87
+ }
88
+ function isSecureApiKey(key) {
89
+ return typeof key === 'string' && key.trim().length >= 16;
90
+ }
91
+ /**
92
+ * Refuse to bind to a non-loopback host without an API key. Throws on
93
+ * policy violation; returns silently when allowed.
94
+ */
95
+ export function assertHttpBindingPolicy(host, apiKey) {
96
+ if (isLoopbackHttpHost(host)) {
97
+ if (apiKey !== undefined && !isSecureApiKey(apiKey)) {
98
+ throw new FsError(ErrorCode.PERMISSION_DENIED, 'API_KEY is configured but is insecure (minimum 16 characters).');
99
+ }
100
+ return;
101
+ }
102
+ if (isSecureApiKey(apiKey))
103
+ return;
104
+ throw new FsError(ErrorCode.PERMISSION_DENIED, `Refusing to bind HTTP server to non-loopback host '${host}' without a secure API_KEY (minimum 16 characters).`);
105
+ }
106
+ /**
107
+ * The Host header values this bind accepts. `FS_ALLOWED_HOSTS` wins
108
+ * when set; otherwise a loopback bind takes the whole localhost hostname set (a
109
+ * client dialing http://localhost:<port> sends `Host: localhost`, which a bare
110
+ * ['127.0.0.1'] list would 403), a wildcard bind takes none, and a concrete
111
+ * non-loopback bind takes itself. An empty result means no Host validation is
112
+ * mounted — only reachable under FS_ALLOW_UNRESTRICTED_HOSTS=1.
113
+ */
114
+ export function resolveAllowedHosts(httpHost, allowedHostsEnv) {
115
+ const configured = splitCsvList(allowedHostsEnv);
116
+ if (configured.length > 0)
117
+ return configured;
118
+ if (isLoopbackHttpHost(httpHost))
119
+ return localhostAllowedHostnames();
120
+ if (isWildcardHttpHost(httpHost))
121
+ return [];
122
+ return [httpHost];
123
+ }
124
+ /**
125
+ * Express's `trust proxy` setting from `FS_TRUST_PROXY`. A
126
+ * non-negative integer hop count parses to a number (Express counts hops from
127
+ * the socket); anything else (a subnet name/expression, or a negative/
128
+ * non-integer string) passes through unchanged for Express to interpret.
129
+ * `undefined` means the env var was unset or empty — leave Express's default
130
+ * (disabled) in place.
131
+ */
132
+ export function resolveTrustProxySetting(value) {
133
+ if (!value)
134
+ return undefined;
135
+ const hops = Number(value);
136
+ return Number.isInteger(hops) && hops >= 0 ? hops : value;
137
+ }
138
+ /**
139
+ * Refuse to bind a wildcard host (`0.0.0.0` / `::`) without an explicit
140
+ * `FS_ALLOWED_HOSTS` list. Clients never send `Host: 0.0.0.0`, so
141
+ * defaulting the allowed-host set to the wildcard string would reject all real
142
+ * traffic. Operators who accept the risk can set
143
+ * `FS_ALLOW_UNRESTRICTED_HOSTS=1` to restore warn-and-bind.
144
+ * Loopback and concrete non-loopback hosts are unaffected.
145
+ */
146
+ export function assertHttpHostPolicy(host, allowedHosts, allowUnrestricted) {
147
+ if (isLoopbackHttpHost(host))
148
+ return;
149
+ // concrete non-loopback: Host validated against the bind host.
150
+ if (!isWildcardHttpHost(host))
151
+ return;
152
+ if (allowUnrestricted)
153
+ return;
154
+ if (allowedHosts.length > 0)
155
+ return;
156
+ throw new FsError(ErrorCode.PERMISSION_DENIED, `Refusing to bind wildcard host '${host}' without FS_ALLOWED_HOSTS. Set it to the public hostname(s) clients send, or set FS_ALLOW_UNRESTRICTED_HOSTS=true to accept the risk.`);
157
+ }
158
+ // ─── Protected-resource discovery (RFC 9728 / RFC 6750) ──────────────────────
159
+ /**
160
+ * This server is a resource server with no authorization server: `API_KEY` is a
161
+ * static secret the operator hands out of band, not an issued token. So the
162
+ * metadata document deliberately omits `authorization_servers` — RFC 9728 §2
163
+ * makes it optional, and its absence is the accurate statement that a token
164
+ * cannot be obtained from an endpoint. `mcpAuthMetadataRouter` from
165
+ * `@modelcontextprotocol/express` is the tool for the IdP-backed case: it
166
+ * requires RFC 8414 authorization-server metadata, and inventing an issuer with
167
+ * endpoints that answer nothing would send clients into a flow that cannot
168
+ * complete. Adopt it if this ever moves to a real IdP.
169
+ *
170
+ * What discovery buys here: a client hitting 401 learns the resource identifier
171
+ * and that the credential goes in the Authorization header, instead of a bare
172
+ * challenge plus a 404 on the well-known path.
173
+ */
174
+ export function protectedResourceUrl(req, hostValidated, configured) {
175
+ if (configured) {
176
+ const parsed = URL.parse(configured);
177
+ if (parsed)
178
+ return parsed;
179
+ Logger.warn(`[HTTP] Ignoring unparseable FS_PUBLIC_URL: ${configured}. Deriving the resource identifier from the Host header instead.`);
180
+ }
181
+ // With no Host validator mounted (FS_ALLOW_UNRESTRICTED_HOSTS=1)
182
+ // the Host header is attacker-controlled, and naming a resource from it would
183
+ // publish an identifier the operator does not own. Answer with nothing instead.
184
+ if (!hostValidated)
185
+ return null;
186
+ const host = req.headers.host ?? '127.0.0.1';
187
+ return URL.parse(`${req.secure ? 'https' : 'http'}://${host}/mcp`);
188
+ }
189
+ /**
190
+ * RFC 6750 §3: a request with no credentials gets a bare challenge, while one
191
+ * that presented something invalid gets `error="invalid_token"`. Clients use
192
+ * the difference to tell "you must authenticate" from "your token is wrong".
193
+ */
194
+ function buildAuthChallenge(req, hasCredentials, hostValidated, publicUrl) {
195
+ const resource = protectedResourceUrl(req, hostValidated, publicUrl);
196
+ const params = [];
197
+ if (resource) {
198
+ params.push(`resource_metadata="${getOAuthProtectedResourceMetadataUrl(resource)}"`);
199
+ }
200
+ if (hasCredentials) {
201
+ params.push('error="invalid_token"', 'error_description="Invalid or expired bearer token"');
202
+ }
203
+ // A bare `Bearer` is a complete challenge; avoid emitting a trailing space.
204
+ return params.length > 0 ? `Bearer ${params.join(', ')}` : 'Bearer';
205
+ }
206
+ /**
207
+ * Express middleware: when `apiKey` is set, require a matching bearer token.
208
+ * No key set = open access (loopback dev mode). `apiKey` is captured once per
209
+ * app setup (passed in from startHttpServer) so the middleware and
210
+ * assertHttpBindingPolicy share one source of truth.
211
+ *
212
+ * `requireBearerAuth` + `verifyAccessToken` from `@modelcontextprotocol/express`
213
+ * is the tool for verifier-backed tokens; this middleware stays hand-rolled
214
+ * because the credential is a static operator key with no verifier, the 401 body
215
+ * must stay a JSON-RPC envelope, and the RFC 6750 bare-vs-`invalid_token`
216
+ * challenge split is implemented here. Adopt `requireBearerAuth` if this ever
217
+ * verifies issued tokens.
218
+ */
219
+ export function bearerAuthMiddleware(apiKey, hostValidated, publicUrl) {
220
+ return (req, res, next) => {
221
+ if (!apiKey) {
222
+ next();
223
+ return;
224
+ }
225
+ const authHeader = req.headers.authorization;
226
+ if (isSecureApiKey(apiKey) && validateBearerAuthorization(apiKey, authHeader)) {
227
+ // Forward the validated caller to the SDK pipeline: toNodeHandler reads
228
+ // req.auth and passes it as the handler's pass-through authInfo, which the
229
+ // per-request factory receives and tool handlers read as ctx.http.authInfo.
230
+ // `expiresAt` is deliberately omitted: a static API key has no expiry and
231
+ // there is no `exp` claim or introspection response to read one from.
232
+ // Populate it from the token if this ever moves to issued tokens.
233
+ const presented = authHeader.slice('Bearer '.length).trim();
234
+ req.auth = {
235
+ token: presented,
236
+ clientId: 'api-key',
237
+ scopes: [],
238
+ };
239
+ next();
240
+ return;
241
+ }
242
+ sendJsonRpcError(res, 401, JSONRPC_SERVER_ERROR, 'Unauthorized', null, {
243
+ 'WWW-Authenticate': buildAuthChallenge(req, req.headers.authorization !== undefined, hostValidated, publicUrl),
244
+ });
245
+ };
246
+ }
247
+ /**
248
+ * Env-derived CORS origins (hostname-form, no scheme/port — matches the SDK
249
+ * app's `allowedOrigins` consumer). Defaults to the SDK's loopback hostname set
250
+ * so loopback browser clients keep working; operators set
251
+ * `FS_ALLOWED_ORIGINS` to allow remote clients on non-loopback
252
+ * binds. An empty value reads as unset, matching how parseAllowedHostsEnv
253
+ * treats an all-empty list. The same set is consulted by corsPreflightHandler
254
+ * so a remote origin is reflected end-to-end in Access-Control-Allow-Origin.
255
+ */
256
+ export function computeAllowedOriginHostnames(originsEnv) {
257
+ // Truthiness, not list length: an all-blank value (" , ") is a configured but
258
+ // empty allow-list — deny every remote origin — while an unset or "" value
259
+ // falls back to the loopback defaults.
260
+ return originsEnv ? splitCsvList(originsEnv) : localhostAllowedHostnames();
261
+ }
262
+ /**
263
+ * Reflect a present Origin on the response if it is allowed — localhost, or
264
+ * in the env-derived `FS_ALLOWED_ORIGINS` set — and avoid
265
+ * emitting a wildcard fallback. Shared by the OPTIONS preflight and the real
266
+ * POST response: `createMcpExpressApp`'s `allowedOrigins` only gates which
267
+ * Origins are accepted, it never sets `Access-Control-Allow-Origin` on the
268
+ * actual response, so without this a browser client would pass preflight and
269
+ * then have the POST response body blocked by CORS.
270
+ */
271
+ function reflectAllowedOrigin(req, res, allowedOriginHostnames) {
272
+ const origin = req.headers.origin;
273
+ if (origin && isOriginAllowed(origin, allowedOriginHostnames)) {
274
+ res.header('Access-Control-Allow-Origin', origin);
275
+ // Key the response by Origin so a CDN/proxy caching one origin's response
276
+ // cannot replay it for a different origin (cache-poison).
277
+ res.header('Vary', 'Origin');
278
+ }
279
+ }
280
+ /** Mounted ahead of the `/mcp` handlers so every response — not just the
281
+ * OPTIONS preflight — carries `Access-Control-Allow-Origin` for an allowed
282
+ * Origin. */
283
+ export function corsOriginMiddleware(allowedOriginHostnames) {
284
+ return (req, res, next) => {
285
+ reflectAllowedOrigin(req, res, allowedOriginHostnames);
286
+ next();
287
+ };
288
+ }
289
+ /**
290
+ * OPTIONS preflight for `/mcp`. The allow-list carries the SEP-2243 standard
291
+ * headers (`mcp-protocol-version`, `mcp-method`, `mcp-name`): SDK clients send
292
+ * all three on every modern request POST and `createMcpHandler` requires them
293
+ * (400 / -32020), so omitting them here would fail every browser preflight —
294
+ * and the HTTP leg is modern-only, so there is no legacy path to fall back to.
295
+ * `mcp-session-id` is deliberately absent: it is 2025-era only and this
296
+ * endpoint rejects legacy traffic.
297
+ */
298
+ export function corsPreflightHandler(allowedOriginHostnames) {
299
+ return (req, res) => {
300
+ reflectAllowedOrigin(req, res, allowedOriginHostnames);
301
+ res.header('Access-Control-Allow-Methods', 'POST, OPTIONS');
302
+ res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, mcp-protocol-version, mcp-method, mcp-name');
303
+ res.status(204).end();
304
+ };
305
+ }
306
+ // Fixed 60s window; never tuned, so it is a constant rather than config plumbing.
307
+ const RATE_LIMIT_WINDOW_MS = 60_000;
308
+ /**
309
+ * Fixed-window per-client-IP rate limiter.
310
+ */
311
+ export function createRateLimiter(max) {
312
+ const buckets = new Map();
313
+ const sweep = setInterval(() => {
314
+ const cutoff = Date.now() - RATE_LIMIT_WINDOW_MS * 2;
315
+ for (const [ip, bucket] of buckets) {
316
+ if (bucket.windowStart < cutoff)
317
+ buckets.delete(ip);
318
+ }
319
+ }, RATE_LIMIT_WINDOW_MS);
320
+ sweep.unref();
321
+ return (req, res, next) => {
322
+ const ip = req.ip ?? req.socket.remoteAddress ?? 'unknown';
323
+ const now = Date.now();
324
+ let bucket = buckets.get(ip);
325
+ if (!bucket || now - bucket.windowStart >= RATE_LIMIT_WINDOW_MS) {
326
+ bucket = { windowStart: now, count: 0 };
327
+ buckets.set(ip, bucket);
328
+ }
329
+ bucket.count += 1;
330
+ if (bucket.count <= max) {
331
+ next();
332
+ return;
333
+ }
334
+ const retryAfterSec = Math.max(1, Math.ceil((bucket.windowStart + RATE_LIMIT_WINDOW_MS - now) / 1000));
335
+ sendJsonRpcError(res, 429, JSONRPC_SERVER_ERROR, 'Rate limit exceeded', null, {
336
+ 'Retry-After': String(retryAfterSec),
337
+ });
338
+ };
339
+ }
340
+ //# sourceMappingURL=http-policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-policy.js","sourceRoot":"","sources":["../src/http-policy.ts"],"names":[],"mappings":"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","sourcesContent":["import { getOAuthProtectedResourceMetadataUrl } from '@modelcontextprotocol/express';\nimport { JSONRPC_VERSION, localhostAllowedHostnames } from '@modelcontextprotocol/server';\nimport type { AuthInfo } from '@modelcontextprotocol/server';\n\nimport { createHash, timingSafeEqual } from 'node:crypto';\n\nimport type { NextFunction, Request, RequestHandler, Response } from 'express';\n\nimport { ErrorCode, FsError } from './core/errors.js';\nimport { Logger } from './core/observability.js';\nimport { splitCsvList } from './core/util.js';\n\nconst MAX_BEARER_TOKEN_LENGTH = 4096;\n\n/**\n * JSON-RPC's server-defined error range. No SDK enum maps to \"Unauthorized\" or\n * \"Rate limit exceeded\", both of which this module refuses before any handler\n * has parsed a request.\n */\nconst JSONRPC_SERVER_ERROR = -32000;\n\n/**\n * The one JSON-RPC error envelope every HTTP refusal goes out in — this\n * module's pre-handler rejections and `transport.ts`'s. It lives here because\n * `transport.ts` already imports this module and not the other way round; a\n * second hand-built literal is how the two drift apart.\n *\n * `headers` carries what a particular refusal owes the client on top of the\n * JSON content type — `WWW-Authenticate` on a 401, `Retry-After` on a 429.\n * Goes out through `res.json` so every refusal keeps the `charset=utf-8` and\n * `Content-Length` a hand-rolled `writeHead`/`end` pair drops.\n */\nexport function sendJsonRpcError(\n res: Response,\n status: number,\n code: number,\n message: string,\n id: string | number | null = null,\n headers: Record<string, string> = {},\n): void {\n res.status(status).set(headers).json({ jsonrpc: JSONRPC_VERSION, id, error: { code, message } });\n}\n\nconst LOCALHOST_ORIGIN_RE = /^https?:\\/\\/(localhost|127\\.0\\.0\\.1|\\[::1\\])(:\\d+)?$/u;\n\n/**\n * Pure HTTP auth and binding policy. Holds no state; all functions are\n * directly testable without spinning up a server.\n */\nexport function isLoopbackHttpHost(host: string): boolean {\n const normalizedHost = host.trim().toLowerCase();\n return (\n normalizedHost === '127.0.0.1' ||\n normalizedHost === 'localhost' ||\n normalizedHost === '[::1]' ||\n normalizedHost === '::1'\n );\n}\n\nexport function isAllowedLocalhostOrigin(origin: string): boolean {\n return LOCALHOST_ORIGIN_RE.test(origin);\n}\n\n/** A bind to every interface. Clients never send `Host: 0.0.0.0`, so this bind\n * cannot derive its allowed-host set from itself. */\nfunction isWildcardHttpHost(host: string): boolean {\n return host === '0.0.0.0' || host === '::';\n}\n\nfunction originHostname(origin: string): string | undefined {\n try {\n return new URL(origin).hostname;\n } catch {\n return undefined;\n }\n}\n\n/**\n * True if `origin` (a raw `Origin` request header) is allowed given the\n * env-derived `allowedHostnames` set (hostname-form, no scheme/port). Localhost\n * origins are always accepted via {@link isAllowedLocalhostOrigin}; a remote\n * origin is accepted iff its parsed hostname is in the set. Both the SDK app's\n * `allowedOrigins` and this OPTIONS-handler check consume hostname-form, so a\n * remote origin allowed via `FS_ALLOWED_ORIGINS` is reflected\n * end-to-end in `Access-Control-Allow-Origin`.\n */\nexport function isOriginAllowed(origin: string, allowedHostnames: readonly string[]): boolean {\n if (isAllowedLocalhostOrigin(origin)) return true;\n const host = originHostname(origin);\n return host !== undefined && allowedHostnames.includes(host);\n}\n\nexport function validateBearerAuthorization(\n apiKey: string,\n authHeader: unknown,\n): authHeader is string {\n const bearerPrefix = 'Bearer ';\n if (typeof authHeader !== 'string' || !authHeader.startsWith(bearerPrefix)) {\n return false;\n }\n const userKey = authHeader.slice(bearerPrefix.length).trim();\n const normalizedApiKey = apiKey.trim();\n if (userKey.length > MAX_BEARER_TOKEN_LENGTH || normalizedApiKey.length === 0) {\n return false;\n }\n\n // Pure: hash per call. createHash is negligible next to the timingSafeEqual\n // already done per request, and avoiding module-level cache state keeps this\n // testable without post-import env-mutation footguns.\n const expectedHash = createHash('sha256').update(normalizedApiKey).digest();\n const actualHash = createHash('sha256').update(userKey).digest();\n return timingSafeEqual(expectedHash, actualHash);\n}\n\nfunction isSecureApiKey(key: string | undefined): boolean {\n return typeof key === 'string' && key.trim().length >= 16;\n}\n\n/**\n * Refuse to bind to a non-loopback host without an API key. Throws on\n * policy violation; returns silently when allowed.\n */\nexport function assertHttpBindingPolicy(host: string, apiKey: string | undefined): void {\n if (isLoopbackHttpHost(host)) {\n if (apiKey !== undefined && !isSecureApiKey(apiKey)) {\n throw new FsError(\n ErrorCode.PERMISSION_DENIED,\n 'API_KEY is configured but is insecure (minimum 16 characters).',\n );\n }\n return;\n }\n if (isSecureApiKey(apiKey)) return;\n throw new FsError(\n ErrorCode.PERMISSION_DENIED,\n `Refusing to bind HTTP server to non-loopback host '${host}' without a secure API_KEY (minimum 16 characters).`,\n );\n}\n\n/**\n * The Host header values this bind accepts. `FS_ALLOWED_HOSTS` wins\n * when set; otherwise a loopback bind takes the whole localhost hostname set (a\n * client dialing http://localhost:<port> sends `Host: localhost`, which a bare\n * ['127.0.0.1'] list would 403), a wildcard bind takes none, and a concrete\n * non-loopback bind takes itself. An empty result means no Host validation is\n * mounted — only reachable under FS_ALLOW_UNRESTRICTED_HOSTS=1.\n */\nexport function resolveAllowedHosts(\n httpHost: string,\n allowedHostsEnv: string | undefined,\n): string[] {\n const configured = splitCsvList(allowedHostsEnv);\n if (configured.length > 0) return configured;\n if (isLoopbackHttpHost(httpHost)) return localhostAllowedHostnames();\n if (isWildcardHttpHost(httpHost)) return [];\n return [httpHost];\n}\n\n/**\n * Express's `trust proxy` setting from `FS_TRUST_PROXY`. A\n * non-negative integer hop count parses to a number (Express counts hops from\n * the socket); anything else (a subnet name/expression, or a negative/\n * non-integer string) passes through unchanged for Express to interpret.\n * `undefined` means the env var was unset or empty — leave Express's default\n * (disabled) in place.\n */\nexport function resolveTrustProxySetting(value: string | undefined): number | string | undefined {\n if (!value) return undefined;\n const hops = Number(value);\n return Number.isInteger(hops) && hops >= 0 ? hops : value;\n}\n\n/**\n * Refuse to bind a wildcard host (`0.0.0.0` / `::`) without an explicit\n * `FS_ALLOWED_HOSTS` list. Clients never send `Host: 0.0.0.0`, so\n * defaulting the allowed-host set to the wildcard string would reject all real\n * traffic. Operators who accept the risk can set\n * `FS_ALLOW_UNRESTRICTED_HOSTS=1` to restore warn-and-bind.\n * Loopback and concrete non-loopback hosts are unaffected.\n */\nexport function assertHttpHostPolicy(\n host: string,\n allowedHosts: readonly string[],\n allowUnrestricted: boolean,\n): void {\n if (isLoopbackHttpHost(host)) return;\n // concrete non-loopback: Host validated against the bind host.\n if (!isWildcardHttpHost(host)) return;\n if (allowUnrestricted) return;\n if (allowedHosts.length > 0) return;\n throw new FsError(\n ErrorCode.PERMISSION_DENIED,\n `Refusing to bind wildcard host '${host}' without FS_ALLOWED_HOSTS. Set it to the public hostname(s) clients send, or set FS_ALLOW_UNRESTRICTED_HOSTS=true to accept the risk.`,\n );\n}\n\n// ─── Protected-resource discovery (RFC 9728 / RFC 6750) ──────────────────────\n\n/**\n * This server is a resource server with no authorization server: `API_KEY` is a\n * static secret the operator hands out of band, not an issued token. So the\n * metadata document deliberately omits `authorization_servers` — RFC 9728 §2\n * makes it optional, and its absence is the accurate statement that a token\n * cannot be obtained from an endpoint. `mcpAuthMetadataRouter` from\n * `@modelcontextprotocol/express` is the tool for the IdP-backed case: it\n * requires RFC 8414 authorization-server metadata, and inventing an issuer with\n * endpoints that answer nothing would send clients into a flow that cannot\n * complete. Adopt it if this ever moves to a real IdP.\n *\n * What discovery buys here: a client hitting 401 learns the resource identifier\n * and that the credential goes in the Authorization header, instead of a bare\n * challenge plus a 404 on the well-known path.\n */\nexport function protectedResourceUrl(\n req: Request,\n hostValidated: boolean,\n configured: string | undefined,\n): URL | null {\n if (configured) {\n const parsed = URL.parse(configured);\n if (parsed) return parsed;\n Logger.warn(\n `[HTTP] Ignoring unparseable FS_PUBLIC_URL: ${configured}. Deriving the resource identifier from the Host header instead.`,\n );\n }\n // With no Host validator mounted (FS_ALLOW_UNRESTRICTED_HOSTS=1)\n // the Host header is attacker-controlled, and naming a resource from it would\n // publish an identifier the operator does not own. Answer with nothing instead.\n if (!hostValidated) return null;\n const host = req.headers.host ?? '127.0.0.1';\n return URL.parse(`${req.secure ? 'https' : 'http'}://${host}/mcp`);\n}\n\n/**\n * RFC 6750 §3: a request with no credentials gets a bare challenge, while one\n * that presented something invalid gets `error=\"invalid_token\"`. Clients use\n * the difference to tell \"you must authenticate\" from \"your token is wrong\".\n */\nfunction buildAuthChallenge(\n req: Request,\n hasCredentials: boolean,\n hostValidated: boolean,\n publicUrl: string | undefined,\n): string {\n const resource = protectedResourceUrl(req, hostValidated, publicUrl);\n const params: string[] = [];\n if (resource) {\n params.push(`resource_metadata=\"${getOAuthProtectedResourceMetadataUrl(resource)}\"`);\n }\n if (hasCredentials) {\n params.push('error=\"invalid_token\"', 'error_description=\"Invalid or expired bearer token\"');\n }\n // A bare `Bearer` is a complete challenge; avoid emitting a trailing space.\n return params.length > 0 ? `Bearer ${params.join(', ')}` : 'Bearer';\n}\n\n/**\n * Express middleware: when `apiKey` is set, require a matching bearer token.\n * No key set = open access (loopback dev mode). `apiKey` is captured once per\n * app setup (passed in from startHttpServer) so the middleware and\n * assertHttpBindingPolicy share one source of truth.\n *\n * `requireBearerAuth` + `verifyAccessToken` from `@modelcontextprotocol/express`\n * is the tool for verifier-backed tokens; this middleware stays hand-rolled\n * because the credential is a static operator key with no verifier, the 401 body\n * must stay a JSON-RPC envelope, and the RFC 6750 bare-vs-`invalid_token`\n * challenge split is implemented here. Adopt `requireBearerAuth` if this ever\n * verifies issued tokens.\n */\nexport function bearerAuthMiddleware(\n apiKey: string | undefined,\n hostValidated: boolean,\n publicUrl?: string,\n): RequestHandler {\n return (req: Request, res: Response, next: NextFunction): void => {\n if (!apiKey) {\n next();\n return;\n }\n const authHeader = req.headers.authorization;\n if (isSecureApiKey(apiKey) && validateBearerAuthorization(apiKey, authHeader)) {\n // Forward the validated caller to the SDK pipeline: toNodeHandler reads\n // req.auth and passes it as the handler's pass-through authInfo, which the\n // per-request factory receives and tool handlers read as ctx.http.authInfo.\n // `expiresAt` is deliberately omitted: a static API key has no expiry and\n // there is no `exp` claim or introspection response to read one from.\n // Populate it from the token if this ever moves to issued tokens.\n const presented = authHeader.slice('Bearer '.length).trim();\n (req as Request & { auth?: AuthInfo }).auth = {\n token: presented,\n clientId: 'api-key',\n scopes: [],\n };\n next();\n return;\n }\n sendJsonRpcError(res, 401, JSONRPC_SERVER_ERROR, 'Unauthorized', null, {\n 'WWW-Authenticate': buildAuthChallenge(\n req,\n req.headers.authorization !== undefined,\n hostValidated,\n publicUrl,\n ),\n });\n };\n}\n\n/**\n * Env-derived CORS origins (hostname-form, no scheme/port — matches the SDK\n * app's `allowedOrigins` consumer). Defaults to the SDK's loopback hostname set\n * so loopback browser clients keep working; operators set\n * `FS_ALLOWED_ORIGINS` to allow remote clients on non-loopback\n * binds. An empty value reads as unset, matching how parseAllowedHostsEnv\n * treats an all-empty list. The same set is consulted by corsPreflightHandler\n * so a remote origin is reflected end-to-end in Access-Control-Allow-Origin.\n */\nexport function computeAllowedOriginHostnames(originsEnv: string | undefined): string[] {\n // Truthiness, not list length: an all-blank value (\" , \") is a configured but\n // empty allow-list — deny every remote origin — while an unset or \"\" value\n // falls back to the loopback defaults.\n return originsEnv ? splitCsvList(originsEnv) : localhostAllowedHostnames();\n}\n\n/**\n * Reflect a present Origin on the response if it is allowed — localhost, or\n * in the env-derived `FS_ALLOWED_ORIGINS` set — and avoid\n * emitting a wildcard fallback. Shared by the OPTIONS preflight and the real\n * POST response: `createMcpExpressApp`'s `allowedOrigins` only gates which\n * Origins are accepted, it never sets `Access-Control-Allow-Origin` on the\n * actual response, so without this a browser client would pass preflight and\n * then have the POST response body blocked by CORS.\n */\nfunction reflectAllowedOrigin(\n req: Request,\n res: Response,\n allowedOriginHostnames: readonly string[],\n): void {\n const origin = req.headers.origin;\n if (origin && isOriginAllowed(origin, allowedOriginHostnames)) {\n res.header('Access-Control-Allow-Origin', origin);\n // Key the response by Origin so a CDN/proxy caching one origin's response\n // cannot replay it for a different origin (cache-poison).\n res.header('Vary', 'Origin');\n }\n}\n\n/** Mounted ahead of the `/mcp` handlers so every response — not just the\n * OPTIONS preflight — carries `Access-Control-Allow-Origin` for an allowed\n * Origin. */\nexport function corsOriginMiddleware(allowedOriginHostnames: readonly string[]): RequestHandler {\n return (req: Request, res: Response, next: NextFunction): void => {\n reflectAllowedOrigin(req, res, allowedOriginHostnames);\n next();\n };\n}\n\n/**\n * OPTIONS preflight for `/mcp`. The allow-list carries the SEP-2243 standard\n * headers (`mcp-protocol-version`, `mcp-method`, `mcp-name`): SDK clients send\n * all three on every modern request POST and `createMcpHandler` requires them\n * (400 / -32020), so omitting them here would fail every browser preflight —\n * and the HTTP leg is modern-only, so there is no legacy path to fall back to.\n * `mcp-session-id` is deliberately absent: it is 2025-era only and this\n * endpoint rejects legacy traffic.\n */\nexport function corsPreflightHandler(allowedOriginHostnames: readonly string[]): RequestHandler {\n return (req: Request, res: Response): void => {\n reflectAllowedOrigin(req, res, allowedOriginHostnames);\n res.header('Access-Control-Allow-Methods', 'POST, OPTIONS');\n res.header(\n 'Access-Control-Allow-Headers',\n 'Content-Type, Authorization, mcp-protocol-version, mcp-method, mcp-name',\n );\n res.status(204).end();\n };\n}\n\n// ─── Rate limiting (public bind only) ────────────────────────────────────────\n\ninterface RateBucket {\n windowStart: number;\n count: number;\n}\n\n// Fixed 60s window; never tuned, so it is a constant rather than config plumbing.\nconst RATE_LIMIT_WINDOW_MS = 60_000;\n\n/**\n * Fixed-window per-client-IP rate limiter.\n */\nexport function createRateLimiter(max: number): RequestHandler {\n const buckets = new Map<string, RateBucket>();\n const sweep = setInterval(() => {\n const cutoff = Date.now() - RATE_LIMIT_WINDOW_MS * 2;\n for (const [ip, bucket] of buckets) {\n if (bucket.windowStart < cutoff) buckets.delete(ip);\n }\n }, RATE_LIMIT_WINDOW_MS);\n sweep.unref();\n\n return (req: Request, res: Response, next: NextFunction): void => {\n const ip = req.ip ?? req.socket.remoteAddress ?? 'unknown';\n const now = Date.now();\n let bucket = buckets.get(ip);\n if (!bucket || now - bucket.windowStart >= RATE_LIMIT_WINDOW_MS) {\n bucket = { windowStart: now, count: 0 };\n buckets.set(ip, bucket);\n }\n bucket.count += 1;\n if (bucket.count <= max) {\n next();\n return;\n }\n const retryAfterSec = Math.max(\n 1,\n Math.ceil((bucket.windowStart + RATE_LIMIT_WINDOW_MS - now) / 1000),\n );\n sendJsonRpcError(res, 429, JSONRPC_SERVER_ERROR, 'Rate limit exceeded', null, {\n 'Retry-After': String(retryAfterSec),\n });\n };\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  #!/usr/bin/env node
2
2
  export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import process from 'node:process';
3
- import { createTimedAbortSignal } from './lib/abort.js';
4
- import { DEFAULT_SEARCH_TIMEOUT_MS } from './lib/constants.js';
5
- import { formatUnknownErrorMessage } from './lib/errors.js';
6
- import { setAllowedDirectoriesResolved } from './lib/paths.js';
7
- import { CliExitError, parseArgs } from './cli.js';
8
- import { createServer, startHttpServer, startServer } from './server.js';
3
+ import * as z from 'zod/v4';
4
+ import { CliExitError, parseArgs, runPrintConfig } from './cli.js';
5
+ import { logRuntimeFailure } from './core/observability.js';
6
+ import { NO_POSITIONAL_ROOTS_GUIDANCE } from './instructions.js';
7
+ import { startHttpServer, startServer } from './transport.js';
8
+ z.config(z.locales.en());
9
9
  const SHUTDOWN_TIMEOUT_MS = 5000;
10
- let activeServer;
10
+ let activeStdioHandle;
11
11
  let activeHttpServer;
12
12
  let shutdownStarted = false;
13
13
  function isStdinEvent(event) {
@@ -21,32 +21,49 @@ function registerShutdownTrigger(event) {
21
21
  });
22
22
  }
23
23
  async function shutdown(reason, exitCode = 0) {
24
- if (shutdownStarted)
24
+ if (shutdownStarted) {
25
+ if (reason === 'SIGINT' || reason === 'SIGTERM') {
26
+ process.exit(130);
27
+ }
25
28
  return;
29
+ }
26
30
  shutdownStarted = true;
27
31
  process.exitCode = exitCode;
28
32
  let keepForceExitTimer = true;
29
33
  const timer = setTimeout(() => {
30
- console.error(`Shutdown timed out (${reason}), forcing exit.`);
34
+ logRuntimeFailure('shutdown_timeout', 'process', 'shutdown', `Shutdown timed out (${reason}), forcing exit.`);
31
35
  process.exit(exitCode);
32
36
  }, SHUTDOWN_TIMEOUT_MS);
33
37
  timer.unref();
34
38
  try {
35
39
  if (activeHttpServer) {
36
40
  const server = activeHttpServer;
37
- await new Promise((resolve) => {
38
- server.close(() => {
39
- resolve();
41
+ try {
42
+ await new Promise((resolve, reject) => {
43
+ server.close((err) => {
44
+ if (err)
45
+ reject(err);
46
+ else
47
+ resolve();
48
+ });
40
49
  });
41
- });
50
+ }
51
+ catch (error) {
52
+ logRuntimeFailure('shutdown_http_error', 'process', 'shutdown', error);
53
+ }
42
54
  }
43
- if (activeServer) {
44
- await activeServer.close();
55
+ if (activeStdioHandle) {
56
+ try {
57
+ await activeStdioHandle.close();
58
+ }
59
+ catch (error) {
60
+ logRuntimeFailure('shutdown_mcp_error', 'process', 'shutdown', error);
61
+ }
45
62
  }
46
63
  keepForceExitTimer = false;
47
64
  }
48
65
  catch (error) {
49
- console.error(`Shutdown error (${reason}):`, formatUnknownErrorMessage(error));
66
+ logRuntimeFailure('shutdown_error', 'process', 'shutdown', error);
50
67
  }
51
68
  finally {
52
69
  if (!keepForceExitTimer) {
@@ -58,64 +75,83 @@ async function main() {
58
75
  let allowedDirs;
59
76
  let allowCwd;
60
77
  let port;
78
+ let readOnly;
79
+ let printConfig;
80
+ let json;
81
+ let httpHost;
82
+ let cliApiKey;
61
83
  try {
62
84
  const parsed = await parseArgs();
63
- ({ allowedDirs, allowCwd, port } = parsed);
85
+ allowedDirs = parsed.allowedDirs;
86
+ allowCwd = parsed.allowCwd;
87
+ port = parsed.port;
88
+ readOnly = parsed.readOnly;
89
+ printConfig = parsed.printConfig;
90
+ json = parsed.json;
91
+ httpHost = parsed.httpHost;
92
+ cliApiKey = parsed.apiKey;
64
93
  }
65
94
  catch (error) {
66
95
  if (error instanceof CliExitError) {
67
96
  if (error.message.length > 0) {
68
- console.error(error.message);
97
+ logRuntimeFailure('cli_exit', 'startup', 'parse_args', error.message);
69
98
  }
70
99
  process.exitCode = error.exitCode;
71
100
  return;
72
101
  }
73
102
  throw error;
74
103
  }
104
+ if (printConfig) {
105
+ const apiKey = cliApiKey;
106
+ await runPrintConfig({
107
+ allowedDirs,
108
+ allowCwd,
109
+ readOnly,
110
+ json,
111
+ ...(port !== undefined ? { port } : {}),
112
+ ...(httpHost !== undefined ? { httpHost } : {}),
113
+ ...(apiKey !== undefined ? { apiKey } : {}),
114
+ });
115
+ return;
116
+ }
75
117
  if (allowedDirs.length > 0) {
76
- const { signal, cleanup } = createTimedAbortSignal(undefined, DEFAULT_SEARCH_TIMEOUT_MS);
77
- try {
78
- await setAllowedDirectoriesResolved(allowedDirs, signal);
79
- }
80
- finally {
81
- cleanup();
82
- }
83
118
  console.error('Allowed directories (from CLI):');
84
119
  for (const dir of allowedDirs) {
85
120
  console.error(`- ${dir}`);
86
121
  }
87
122
  }
88
123
  else {
89
- console.error(`No directories specified via CLI. Will use MCP Roots${allowCwd ? ' or current working directory' : ''}.`);
124
+ console.error(NO_POSITIONAL_ROOTS_GUIDANCE);
125
+ }
126
+ if (readOnly) {
127
+ console.error('Read-only mode: mutating tools disabled.');
90
128
  }
129
+ const serverOptions = { allowCwd, cliAllowedDirs: allowedDirs, readOnly };
130
+ const runtimeConfig = {
131
+ ...(httpHost !== undefined ? { httpHost } : {}),
132
+ ...(cliApiKey !== undefined ? { apiKey: cliApiKey } : {}),
133
+ };
91
134
  if (port !== undefined) {
92
- activeHttpServer = await startHttpServer(port, {
93
- allowCwd,
94
- cliAllowedDirs: allowedDirs,
95
- });
135
+ activeHttpServer = await startHttpServer(port, serverOptions, runtimeConfig);
96
136
  }
97
137
  else {
98
- const server = await createServer({
99
- allowCwd,
100
- cliAllowedDirs: allowedDirs,
101
- });
102
- activeServer = server;
103
- await startServer(server);
138
+ registerShutdownTrigger('end');
139
+ registerShutdownTrigger('close');
140
+ activeStdioHandle = startServer(serverOptions, runtimeConfig);
104
141
  }
105
142
  }
106
143
  registerShutdownTrigger('SIGTERM');
107
144
  registerShutdownTrigger('SIGINT');
108
- registerShutdownTrigger('end');
109
- registerShutdownTrigger('close');
110
145
  process.once('unhandledRejection', (reason) => {
111
- console.error('Unhandled rejection:', formatUnknownErrorMessage(reason));
146
+ logRuntimeFailure('unhandled_rejection', 'process', 'unhandledRejection', reason);
112
147
  void shutdown('unhandledRejection', 1);
113
148
  });
114
149
  process.once('uncaughtException', (error) => {
115
- console.error('Uncaught exception:', error);
150
+ logRuntimeFailure('uncaught_exception', 'process', 'uncaughtException', error);
116
151
  void shutdown('uncaughtException', 1);
117
152
  });
118
153
  main().catch((error) => {
119
- console.error('Fatal error:', formatUnknownErrorMessage(error));
154
+ logRuntimeFailure('fatal', 'startup', 'main', error);
120
155
  void shutdown('fatal', 1);
121
156
  });
157
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n// Type-only imports are erased at runtime — safe to keep static.\nimport type { StdioServerHandle } from '@modelcontextprotocol/server/stdio';\n\nimport type * as http from 'node:http';\nimport process from 'node:process';\n\nimport * as z from 'zod/v4';\n\nimport { CliExitError, parseArgs, runPrintConfig } from './cli.js';\nimport { logRuntimeFailure } from './core/observability.js';\nimport { NO_POSITIONAL_ROOTS_GUIDANCE } from './instructions.js';\nimport { startHttpServer, startServer } from './transport.js';\n\nz.config(z.locales.en());\n\nconst SHUTDOWN_TIMEOUT_MS = 5000;\nlet activeStdioHandle: StdioServerHandle | undefined;\nlet activeHttpServer: http.Server | undefined;\nlet shutdownStarted = false;\n\nfunction isStdinEvent(event: NodeJS.Signals | 'end' | 'close'): boolean {\n return event === 'end' || event === 'close';\n}\n\nfunction registerShutdownTrigger(event: NodeJS.Signals | 'end' | 'close'): void {\n const target = isStdinEvent(event) ? process.stdin : process;\n target.once(event, () => {\n const reason = isStdinEvent(event) ? `stdin ${event}` : event;\n void shutdown(reason, 0);\n });\n}\n\nasync function shutdown(reason: string, exitCode = 0): Promise<void> {\n if (shutdownStarted) {\n if (reason === 'SIGINT' || reason === 'SIGTERM') {\n process.exit(130);\n }\n return;\n }\n shutdownStarted = true;\n\n process.exitCode = exitCode;\n let keepForceExitTimer = true;\n\n const timer = setTimeout(() => {\n logRuntimeFailure(\n 'shutdown_timeout',\n 'process',\n 'shutdown',\n `Shutdown timed out (${reason}), forcing exit.`,\n );\n process.exit(exitCode);\n }, SHUTDOWN_TIMEOUT_MS);\n timer.unref();\n\n try {\n if (activeHttpServer) {\n const server = activeHttpServer;\n try {\n await new Promise<void>((resolve, reject) => {\n server.close((err) => {\n if (err) reject(err);\n else resolve();\n });\n });\n } catch (error: unknown) {\n logRuntimeFailure('shutdown_http_error', 'process', 'shutdown', error);\n }\n }\n if (activeStdioHandle) {\n try {\n await activeStdioHandle.close();\n } catch (error: unknown) {\n logRuntimeFailure('shutdown_mcp_error', 'process', 'shutdown', error);\n }\n }\n keepForceExitTimer = false;\n } catch (error: unknown) {\n logRuntimeFailure('shutdown_error', 'process', 'shutdown', error);\n } finally {\n if (!keepForceExitTimer) {\n clearTimeout(timer);\n }\n }\n}\n\nasync function main(): Promise<void> {\n let allowedDirs: string[];\n let allowCwd: boolean;\n let port: number | undefined;\n let readOnly: boolean;\n let printConfig: boolean;\n let json: boolean;\n let httpHost: string | undefined;\n let cliApiKey: string | undefined;\n\n try {\n const parsed = await parseArgs();\n allowedDirs = parsed.allowedDirs;\n allowCwd = parsed.allowCwd;\n port = parsed.port;\n readOnly = parsed.readOnly;\n printConfig = parsed.printConfig;\n json = parsed.json;\n httpHost = parsed.httpHost;\n cliApiKey = parsed.apiKey;\n } catch (error: unknown) {\n if (error instanceof CliExitError) {\n if (error.message.length > 0) {\n logRuntimeFailure('cli_exit', 'startup', 'parse_args', error.message);\n }\n process.exitCode = error.exitCode;\n return;\n }\n throw error;\n }\n\n if (printConfig) {\n const apiKey = cliApiKey;\n await runPrintConfig({\n allowedDirs,\n allowCwd,\n readOnly,\n json,\n ...(port !== undefined ? { port } : {}),\n ...(httpHost !== undefined ? { httpHost } : {}),\n ...(apiKey !== undefined ? { apiKey } : {}),\n });\n return;\n }\n\n if (allowedDirs.length > 0) {\n console.error('Allowed directories (from CLI):');\n for (const dir of allowedDirs) {\n console.error(`- ${dir}`);\n }\n } else {\n console.error(NO_POSITIONAL_ROOTS_GUIDANCE);\n }\n\n if (readOnly) {\n console.error('Read-only mode: mutating tools disabled.');\n }\n\n const serverOptions = { allowCwd, cliAllowedDirs: allowedDirs, readOnly };\n const runtimeConfig = {\n ...(httpHost !== undefined ? { httpHost } : {}),\n ...(cliApiKey !== undefined ? { apiKey: cliApiKey } : {}),\n };\n\n if (port !== undefined) {\n activeHttpServer = await startHttpServer(port, serverOptions, runtimeConfig);\n } else {\n registerShutdownTrigger('end');\n registerShutdownTrigger('close');\n activeStdioHandle = startServer(serverOptions, runtimeConfig);\n }\n}\n\nregisterShutdownTrigger('SIGTERM');\nregisterShutdownTrigger('SIGINT');\n\nprocess.once('unhandledRejection', (reason: unknown) => {\n logRuntimeFailure('unhandled_rejection', 'process', 'unhandledRejection', reason);\n void shutdown('unhandledRejection', 1);\n});\n\nprocess.once('uncaughtException', (error: Error) => {\n logRuntimeFailure('uncaught_exception', 'process', 'uncaughtException', error);\n void shutdown('uncaughtException', 1);\n});\n\nmain().catch((error: unknown) => {\n logRuntimeFailure('fatal', 'startup', 'main', error);\n void shutdown('fatal', 1);\n});\n"]}
@@ -0,0 +1,12 @@
1
+ export declare const INSTRUCTIONS_URI = "internal://instructions";
2
+ export declare const NO_POSITIONAL_ROOTS_GUIDANCE = "No positional directories specified. Configure roots with directory arguments, FS_ALLOWED_DIRS, or --allow-cwd. Modern clients with elicitation can also call a tool with a concrete path and approve the requested grant.";
3
+ /**
4
+ * The one statement of what this server is and how to approach it. The server's
5
+ * `instructions`, the `get-help` prompt, and the instructions resource all
6
+ * describe the same document, and three hand-maintained paraphrases of it drift
7
+ * against each other and cost the client the same sentence three times.
8
+ */
9
+ export declare const INSTRUCTIONS_SUMMARY: string;
10
+ export declare function buildSectionsRecord(readOnly: boolean): Record<string, string>;
11
+ export declare function renderSections(sections: Record<string, string>): string;
12
+ //# sourceMappingURL=instructions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instructions.d.ts","sourceRoot":"","sources":["../src/instructions.ts"],"names":[],"mappings":"AAiCA,eAAO,MAAM,gBAAgB,4BAA4B,CAAC;AAC1D,eAAO,MAAM,4BAA4B,+NACqL,CAAC;AAE/N;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,QAEmF,CAAC;AAErH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAuC7E;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAEvE"}