@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
package/dist/resources.js CHANGED
@@ -1,179 +1,408 @@
1
- import { ProtocolError, ProtocolErrorCode, ResourceTemplate, } from '@modelcontextprotocol/server';
2
- import { globalMetrics } from './lib/observability.js';
3
- import { buildToolCatalog } from './resources/tool-catalog.js';
4
- import { buildToolInfo, getToolContracts } from './resources/tool-info.js';
5
- import { buildWorkflowGuide } from './resources/workflows.js';
6
- import { withDefaultIcons } from './tools/shared.js';
7
- const RESULT_TEMPLATE = new ResourceTemplate('filesystem-mcp://result/{id}', {
8
- list: undefined,
9
- });
10
- const TOOL_INFO_TEMPLATE = new ResourceTemplate('internal://tool-info/{name}', {
11
- list: () => ({
12
- resources: getToolContracts().map((contract) => ({
13
- uri: `internal://tool-info/${contract.name}`,
14
- name: contract.name,
15
- title: contract.title,
16
- description: contract.description,
17
- mimeType: 'text/markdown',
18
- })),
19
- }),
20
- });
21
- const TOOL_INFO_RESOURCE_NAME = 'filesystem-mcp-tool-info';
22
- const TOOL_INFO_RESOURCE_DESCRIPTION = 'Per-tool contract details, nuances, and gotchas. Read internal://tool-info/{name} with a tool name such as "read", "ls", or "grep".';
23
- const INSTRUCTIONS_RESOURCE_NAME = 'filesystem-mcp-instructions';
24
- const INSTRUCTIONS_RESOURCE_URI = 'internal://instructions';
25
- const INSTRUCTIONS_RESOURCE_DESCRIPTION = 'Comprehensive rules and guidelines for filesystem-mcp usage.';
26
- const RESULT_RESOURCE_NAME = 'filesystem-mcp-result';
27
- const RESULT_RESOURCE_DESCRIPTION = 'Ephemeral cached tool output exposed as an MCP resource. Not guaranteed to be listed via resources/list.';
28
- const METRICS_RESOURCE_NAME = 'filesystem-mcp-metrics';
29
- const METRICS_RESOURCE_URI = 'filesystem-mcp://metrics';
30
- const METRICS_RESOURCE_DESCRIPTION = 'Live per-tool call/error/avgDurationMs metrics snapshot.';
31
- const CATALOG_RESOURCE_NAME = 'filesystem-mcp-catalog';
32
- const CATALOG_RESOURCE_URI = 'internal://tool-catalog';
33
- const CATALOG_RESOURCE_DESCRIPTION = 'Tool selection guide and data flow map.';
34
- const WORKFLOW_RESOURCE_NAME = 'filesystem-mcp-workflows';
35
- const WORKFLOW_RESOURCE_URI = 'internal://workflows';
36
- const WORKFLOW_RESOURCE_DESCRIPTION = 'Standard operating procedures for exploration, search, edit, and patch.';
37
- export function registerInstructionResource(server, instructions, iconInfo) {
38
- server.registerResource(INSTRUCTIONS_RESOURCE_NAME, INSTRUCTIONS_RESOURCE_URI, withDefaultIcons({
1
+ import { checkResourceAllowed, ProtocolError, ProtocolErrorCode, ResourceNotFoundError, ResourceTemplate, resourceUrlFromServerUrl, UriTemplate, } from '@modelcontextprotocol/server';
2
+ import { ErrorCode, formatUnknownErrorMessage, fsErrorCode, hasErrorShape, isFsError, } from './core/errors.js';
3
+ import { decodeFileUriPath, encodeFileUriPath, extractPath, FILESYSTEM_FILE_URI_TEMPLATE, } from './core/file-uri.js';
4
+ import { GuardedFileSystem } from './core/fs.js';
5
+ import { Logger } from './core/observability.js';
6
+ import { PathCompleter } from './core/path-completer.js';
7
+ import { createWatcherRegistry, MAX_WATCHERS, } from './core/watcher-registry.js';
8
+ import { buildSectionsRecord, INSTRUCTIONS_SUMMARY, INSTRUCTIONS_URI, renderSections, } from './instructions.js';
9
+ /**
10
+ * A filesystem failure that reads as not-found on the resource wire, for both
11
+ * `resources/read` and `resources/subscribe`. ACCESS_DENIED folds in
12
+ * deliberately: masking whether an out-of-root or denylisted path exists is the
13
+ * security-correct answer, and the `FsError` message still carries the real
14
+ * cause to the client.
15
+ */
16
+ function isNotFoundish(error) {
17
+ return (isFsError(error) &&
18
+ (error.code === ErrorCode.NOT_FOUND || error.code === ErrorCode.ACCESS_DENIED));
19
+ }
20
+ // ═══════════════════════════════════════════════════════════════
21
+ // instructions
22
+ // ═══════════════════════════════════════════════════════════════
23
+ function createInstructionsResource(options) {
24
+ const text = renderSections(buildSectionsRecord(options.readOnly));
25
+ return {
26
+ name: 'filesystem-mcp-instructions',
39
27
  title: 'Server Instructions',
40
- description: INSTRUCTIONS_RESOURCE_DESCRIPTION,
28
+ description: INSTRUCTIONS_SUMMARY,
41
29
  mimeType: 'text/markdown',
42
- annotations: {
43
- audience: ['assistant'],
44
- priority: 0.8,
30
+ uri: INSTRUCTIONS_URI,
31
+ annotations: { audience: ['assistant'], priority: 0.8 },
32
+ cacheHint: { cacheScope: options.cacheScope ?? 'public', ttlMs: 300_000 },
33
+ read(uri) {
34
+ return {
35
+ contents: [
36
+ {
37
+ uri: uri.href,
38
+ mimeType: 'text/markdown',
39
+ text,
40
+ },
41
+ ],
42
+ };
45
43
  },
46
- }, iconInfo), (uri) => ({
47
- contents: [
48
- {
49
- uri: uri.href,
50
- mimeType: 'text/markdown',
51
- text: instructions,
52
- },
53
- ],
54
- }));
44
+ };
55
45
  }
56
- export function registerToolCatalogResource(server, iconInfo) {
57
- server.registerResource(CATALOG_RESOURCE_NAME, CATALOG_RESOURCE_URI, withDefaultIcons({
58
- title: 'Tool Catalog',
59
- description: CATALOG_RESOURCE_DESCRIPTION,
60
- mimeType: 'text/markdown',
61
- annotations: {
62
- audience: ['assistant'],
63
- priority: 0.7,
46
+ // ═══════════════════════════════════════════════════════════════
47
+ // filesystem
48
+ // ═══════════════════════════════════════════════════════════════
49
+ function createFilesystemResource(options) {
50
+ const completer = options.pathGuard ? new PathCompleter(options.pathGuard) : undefined;
51
+ const registry = options.watcherRegistry ?? createWatcherRegistry();
52
+ // Only the per-server (legacy/stdio) registry is owned by this resource and
53
+ // destroyed on dispose; the shared modern-leg registry is owned by the host
54
+ // and torn down at server shutdown.
55
+ const ownsRegistry = options.watcherRegistry === undefined;
56
+ // The URIs this connection holds a `resources/subscribe` lease for. The wire
57
+ // verb is per-URI and idempotent — a second subscribe is the same
58
+ // subscription, and one unsubscribe ends it — but the registry ref-counts
59
+ // leases (the HTTP listen leg needs that). Without this set a double
60
+ // subscribe took two leases that one unsubscribe could not release, and an
61
+ // unsubscribe for a URI never subscribed released a lease this connection
62
+ // never took, dropping a watcher some other holder still wanted.
63
+ const leasedUris = new Set();
64
+ // Subscribes mid-acquire, keyed by URI. Two concurrent subscribes for the
65
+ // same URI must share one acquire: both passing the `leasedUris` check
66
+ // before either finished took two registry leases that the single set entry
67
+ // could release only once — on the shared modern registry that orphan lease
68
+ // pinned a watcher until process shutdown.
69
+ const inflight = new Map();
70
+ return {
71
+ name: 'filesystem-mcp-file',
72
+ title: 'Workspace File',
73
+ description: 'Read a file from the workspace. Subscribe to get updates when the file changes.',
74
+ uriTemplate: FILESYSTEM_FILE_URI_TEMPLATE,
75
+ annotations: { audience: ['assistant'], priority: 0.8 },
76
+ // Without this the SDK falls back to its conservative `{ ttlMs: 0,
77
+ // cacheScope: 'private' }`, making every file read uncacheable — and this
78
+ // is the one resource with a live invalidation channel, so TTL and the
79
+ // watcher compose exactly as the spec's caching section describes. The TTL
80
+ // stays short because a non-subscriber gets no invalidation signal at all.
81
+ cacheHint: { cacheScope: options.cacheScope ?? 'public', ttlMs: 5_000 },
82
+ // No `list`: the template registers with `list: undefined` — readable but
83
+ // not enumerable. Listing each allowed root as a concrete resource
84
+ // duplicated the template and grew resources/list linearly with roots;
85
+ // `list_roots` owns root discovery.
86
+ async read(uri, _variables, _ctx) {
87
+ if (!options.pathGuard) {
88
+ throw new ProtocolError(ProtocolErrorCode.InternalError, 'PathGuard not configured');
89
+ }
90
+ // Decode via extractPath — the same decoder resources/subscribe uses — so
91
+ // both consumers are symmetric with buildFileResourceUri's encoding. The
92
+ // {+path} template variable arrives still percent-encoded, so validating
93
+ // it directly would treat "c%3A/proj/a.ts" as a literal relative path;
94
+ // no fallback to it here, or the two branches would validate different
95
+ // strings. extractPath only fails past the template match on undecodable
96
+ // percent-encoding, which is the caller's malformed URI.
97
+ const rawPath = extractPath(uri.href);
98
+ if (rawPath === undefined) {
99
+ throw new ProtocolError(ProtocolErrorCode.InvalidParams, 'Malformed file URI: path is not valid percent-encoding');
100
+ }
101
+ await options.pathGuard.validateExistingPath(rawPath);
102
+ const fs = new GuardedFileSystem(options.pathGuard);
103
+ const readResult = await fs.readRaw(rawPath);
104
+ return {
105
+ contents: [
106
+ {
107
+ uri: uri.href,
108
+ mimeType: readResult.mimeType || 'application/octet-stream',
109
+ ...(readResult.isBinary
110
+ ? { blob: readResult.content.toString('base64') }
111
+ : { text: readResult.content.toString('utf-8') }),
112
+ },
113
+ ],
114
+ };
64
115
  },
65
- }, iconInfo), (uri) => ({
66
- contents: [
67
- {
68
- uri: uri.href,
69
- mimeType: 'text/markdown',
70
- text: buildToolCatalog(),
71
- },
72
- ],
73
- }));
74
- }
75
- export function registerWorkflowGuideResource(server, iconInfo) {
76
- server.registerResource(WORKFLOW_RESOURCE_NAME, WORKFLOW_RESOURCE_URI, withDefaultIcons({
77
- title: 'Workflow Guide',
78
- description: WORKFLOW_RESOURCE_DESCRIPTION,
79
- mimeType: 'text/markdown',
80
- annotations: {
81
- audience: ['assistant'],
82
- priority: 0.6,
116
+ async complete(variable, value) {
117
+ if (variable !== 'path' || !completer)
118
+ return [];
119
+ // Both ends speak the `{+path}` form (see encodeFileUriPath), not raw OS
120
+ // paths: the partial arriving here is whatever this returned last, so the
121
+ // decode mirrors the encode. An undecodable partial is matched as typed.
122
+ const suggestions = await completer.suggest(decodeFileUriPath(value) ?? value);
123
+ return suggestions.map(encodeFileUriPath);
83
124
  },
84
- }, iconInfo), (uri) => ({
85
- contents: [
86
- {
87
- uri: uri.href,
88
- mimeType: 'text/markdown',
89
- text: buildWorkflowGuide(),
90
- },
91
- ],
92
- }));
125
+ subscribe(uri, notify) {
126
+ const pathGuard = options.pathGuard;
127
+ if (!pathGuard)
128
+ return undefined;
129
+ // Already subscribed on this connection: the watcher is live and the sink
130
+ // is registered, so this is a no-op success rather than a second lease.
131
+ if (leasedUris.has(uri))
132
+ return undefined;
133
+ // A subscribe for this URI is mid-acquire: share its outcome instead of
134
+ // taking a second lease the lease set cannot represent.
135
+ const pending = inflight.get(uri);
136
+ if (pending)
137
+ return pending;
138
+ const acquire = async () => {
139
+ const result = await registry.acquire(pathGuard, uri, notify, {
140
+ markSubscribe: true,
141
+ });
142
+ if (result.ok) {
143
+ leasedUris.add(uri);
144
+ return undefined;
145
+ }
146
+ if (result.reason === 'bad-uri') {
147
+ throw new ResourceNotFoundError(uri, `Cannot subscribe: not a filesystem URI`);
148
+ }
149
+ if (result.reason === 'invalid-path') {
150
+ const err = result.error;
151
+ if (isNotFoundish(err)) {
152
+ throw new ResourceNotFoundError(uri, `Cannot subscribe to ${uri}: ${err.message}`);
153
+ }
154
+ Logger.warn(`Unexpected error validating path for watcher ${uri}: ${formatUnknownErrorMessage(err)}`);
155
+ throw err;
156
+ }
157
+ // Unsubscribed (or the registry destroyed) mid-await: the caller already
158
+ // asked for this to stop, so there is nothing to reject.
159
+ if (result.reason === 'stale')
160
+ return undefined;
161
+ // capped / attach-failed: `false` tells the handler to reject, since
162
+ // undefined would report success with no watcher attached.
163
+ return false;
164
+ };
165
+ const promise = acquire().finally(() => inflight.delete(uri));
166
+ inflight.set(uri, promise);
167
+ return promise;
168
+ },
169
+ unsubscribe(uri) {
170
+ // Only release a lease this connection actually took.
171
+ if (!leasedUris.delete(uri))
172
+ return;
173
+ registry.release(uri);
174
+ },
175
+ destroy() {
176
+ // Release what this connection still holds before the registry goes: on
177
+ // the shared (modern) registry nothing else would ever end these leases.
178
+ for (const uri of leasedUris)
179
+ registry.release(uri);
180
+ leasedUris.clear();
181
+ if (ownsRegistry)
182
+ registry.destroy();
183
+ },
184
+ };
93
185
  }
94
- export function registerResultResources(server, store, iconInfo) {
95
- server.registerResource(RESULT_RESOURCE_NAME, RESULT_TEMPLATE, withDefaultIcons({
186
+ // ═══════════════════════════════════════════════════════════════
187
+ // result
188
+ // ═══════════════════════════════════════════════════════════════
189
+ function createResultResource(options) {
190
+ return {
191
+ name: 'filesystem-mcp-result',
96
192
  title: 'Cached Tool Result',
97
- description: RESULT_RESOURCE_DESCRIPTION,
98
- mimeType: 'text/plain',
99
- annotations: {
100
- audience: ['assistant'],
101
- priority: 0.3,
193
+ description: 'Ephemeral cached tool output. Listed via resources/list; entries expire after the cache TTL.',
194
+ mimeType: 'application/json',
195
+ uriTemplate: 'filesystem-mcp://result/{id}',
196
+ annotations: { audience: ['assistant'], priority: 0.3 },
197
+ cacheHint: { cacheScope: 'private', ttlMs: 60_000 },
198
+ list() {
199
+ const store = options.resourceStore;
200
+ const uris = store.keys(); // prunes expired first
201
+ const resources = [];
202
+ for (const uri of uris) {
203
+ try {
204
+ const entry = store.getEntry(uri);
205
+ resources.push({
206
+ uri: entry.uri,
207
+ name: entry.name,
208
+ mimeType: entry.mimeType,
209
+ size: entry.size,
210
+ });
211
+ }
212
+ catch (err) {
213
+ // An entry may expire between keys() and getEntry; skip it.
214
+ if (isFsError(err) && err.code === ErrorCode.NOT_FOUND)
215
+ continue;
216
+ throw err;
217
+ }
218
+ }
219
+ // Recent-first: keys() returns insertion order, so the newest is last.
220
+ resources.reverse();
221
+ return { resources };
102
222
  },
103
- }, iconInfo), (uri, variables) => {
104
- const { id } = variables;
105
- if (typeof id !== 'string' || id.length === 0) {
106
- throw new ProtocolError(ProtocolErrorCode.ResourceNotFound, 'Cached result expired. Re-run the tool to regenerate.');
107
- }
108
- const entry = store.getText(uri.toString());
109
- return {
110
- contents: [
111
- {
112
- uri: entry.uri,
113
- mimeType: entry.mimeType,
114
- text: entry.text,
115
- },
116
- ],
117
- };
118
- });
119
- }
120
- export function registerToolInfoResource(server, iconInfo) {
121
- server.registerResource(TOOL_INFO_RESOURCE_NAME, TOOL_INFO_TEMPLATE, withDefaultIcons({
122
- title: 'Tool Info',
123
- description: TOOL_INFO_RESOURCE_DESCRIPTION,
124
- mimeType: 'text/markdown',
125
- annotations: {
126
- audience: ['assistant'],
127
- priority: 0.65,
223
+ read(uri, variables) {
224
+ const { id } = variables;
225
+ if (typeof id !== 'string' || id.length === 0) {
226
+ throw new ProtocolError(ProtocolErrorCode.InvalidParams, 'Malformed result URI: missing {id}. Use the exact resourceUri returned by the tool.');
227
+ }
228
+ let entry;
229
+ try {
230
+ entry = options.resourceStore.getEntry(uri.toString());
231
+ }
232
+ catch (err) {
233
+ if (isFsError(err) && err.code === ErrorCode.NOT_FOUND) {
234
+ throw new ResourceNotFoundError(uri.toString(), 'Cached result not found or expired. Re-run the tool to regenerate.');
235
+ }
236
+ throw err;
237
+ }
238
+ return {
239
+ contents: [{ uri: entry.uri, mimeType: entry.mimeType, text: entry.text }],
240
+ };
128
241
  },
129
- }, iconInfo), (uri, variables) => {
130
- const { name } = variables;
131
- if (typeof name !== 'string' || name.length === 0) {
132
- throw new ProtocolError(ProtocolErrorCode.InvalidParams, 'Tool name is required');
242
+ };
243
+ }
244
+ // ═══════════════════════════════════════════════════════════════
245
+ // export contracts
246
+ // ═══════════════════════════════════════════════════════════════
247
+ export function getResourceContracts(options) {
248
+ return [
249
+ createInstructionsResource(options),
250
+ createResultResource(options),
251
+ createFilesystemResource(options),
252
+ ];
253
+ }
254
+ // ═══════════════════════════════════════════════════════════════
255
+ // registrar
256
+ // ═══════════════════════════════════════════════════════════════
257
+ function wrapRead(contract) {
258
+ return async (uri, variables, ctx) => {
259
+ try {
260
+ return await contract.read(uri, variables, ctx);
133
261
  }
134
- const content = buildToolInfo(name);
135
- if (content === undefined) {
136
- throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Tool not found: ${name}`);
262
+ catch (error) {
263
+ if (hasErrorShape(error, 'ProtocolError'))
264
+ throw error;
265
+ // A missing or out-of-root path is a not-found, not a malformed request.
266
+ // The SDK puts ResourceNotFoundError on the wire as -32602 with
267
+ // `data.uri`, which is what clients match on; -32002 is the older code
268
+ // they also accept, and this SDK never emits it.
269
+ if (isNotFoundish(error)) {
270
+ throw new ResourceNotFoundError(uri.toString(), error.message);
271
+ }
272
+ // A remaining FsError (NOT_FILE, TOO_LARGE, ...) traces to the
273
+ // caller-supplied URI; anything else is a server-side failure and must
274
+ // not be blamed on the request.
275
+ const msg = isFsError(error) ? error.message : formatUnknownErrorMessage(error);
276
+ throw new ProtocolError(fsErrorCode(error), msg);
137
277
  }
138
- return {
139
- contents: [
140
- {
141
- uri: uri.href,
142
- mimeType: 'text/markdown',
143
- text: content,
144
- },
145
- ],
146
- };
147
- });
278
+ };
148
279
  }
149
- export function registerMetricsResource(server, iconInfo) {
150
- server.registerResource(METRICS_RESOURCE_NAME, METRICS_RESOURCE_URI, withDefaultIcons({
151
- title: 'Tool Metrics',
152
- description: METRICS_RESOURCE_DESCRIPTION,
153
- mimeType: 'application/json',
154
- annotations: {
155
- audience: ['assistant'],
156
- priority: 0.5,
157
- },
158
- }, iconInfo), (uri) => {
159
- const snapshot = {};
160
- for (const [tool, m] of globalMetrics) {
161
- snapshot[tool] = {
162
- calls: m.calls,
163
- errors: m.errors,
164
- avgDurationMs: m.calls > 0
165
- ? parseFloat((m.totalDurationMs / m.calls).toFixed(2))
166
- : 0,
167
- };
280
+ export function registerResources(deps) {
281
+ const server = deps.server;
282
+ const resourceContracts = getResourceContracts({ ...deps, readOnly: deps.readOnly ?? false });
283
+ for (const contract of resourceContracts) {
284
+ const config = {
285
+ ...(contract.title !== undefined ? { title: contract.title } : {}),
286
+ ...(contract.description !== undefined ? { description: contract.description } : {}),
287
+ ...(contract.mimeType !== undefined ? { mimeType: contract.mimeType } : {}),
288
+ ...(contract.annotations !== undefined ? { annotations: contract.annotations } : {}),
289
+ ...(contract.cacheHint !== undefined ? { cacheHint: contract.cacheHint } : {}),
290
+ };
291
+ if (contract.uriTemplate) {
292
+ const template = new ResourceTemplate(contract.uriTemplate, {
293
+ list: contract.list,
294
+ ...(contract.complete
295
+ ? {
296
+ complete: Object.fromEntries(new UriTemplate(contract.uriTemplate).variableNames.map((varName) => [
297
+ varName,
298
+ (value, ctx) => {
299
+ const completeFn = contract.complete;
300
+ return completeFn ? completeFn(varName, value, ctx) : [];
301
+ },
302
+ ])),
303
+ }
304
+ : {}),
305
+ });
306
+ server.registerResource(contract.name, template, config, wrapRead(contract));
307
+ }
308
+ else if (contract.uri) {
309
+ server.registerResource(contract.name, contract.uri, config, (uri, ctx) => wrapRead(contract)(uri, {}, ctx));
168
310
  }
169
- return {
170
- contents: [
171
- {
172
- uri: uri.href,
173
- mimeType: 'application/json',
174
- text: JSON.stringify({ ok: true, metrics: snapshot }, null, 2),
175
- },
176
- ],
311
+ }
312
+ // `resources/subscribe`/`unsubscribe` are 2025-era-only verbs; a modern
313
+ // server answers `-32601 Method not found` for them, so registering these
314
+ // handlers on a modern-era instance would dispatch to code no request can
315
+ // reach.
316
+ if (deps.era !== 'modern') {
317
+ server.server.assertCanSetRequestHandler('resources/subscribe');
318
+ server.server.assertCanSetRequestHandler('resources/unsubscribe');
319
+ // One stable callback for the whole registrar, NOT one per subscribe. The
320
+ // registry holds these in a Set keyed by identity, so a fresh closure per
321
+ // request made a second `resources/subscribe` for the same URI register a
322
+ // second sink: one file change then sent N notifications, and `unsubscribe`
323
+ // (which only ends a lease) removed none of them. Subscribe is per-URI on
324
+ // the wire, so the sink must be too — this makes it idempotent.
325
+ const notifyUpdated = (updatedUri) => {
326
+ if (deps.notifier) {
327
+ deps.notifier.resourceUpdated(updatedUri);
328
+ return;
329
+ }
330
+ const updatePayload = { uri: updatedUri };
331
+ // A failed notify means the connection went away; nothing to recover.
332
+ void server.server.sendResourceUpdated(updatePayload).catch((err) => {
333
+ Logger.debug('resource update not delivered', {
334
+ uri: updatedUri,
335
+ error: formatUnknownErrorMessage(err),
336
+ });
337
+ });
177
338
  };
178
- });
339
+ server.server.setRequestHandler('resources/subscribe', async (req) => {
340
+ const requestedResource = resourceUrlFromServerUrl(req.params.uri);
341
+ let foundMatch = false;
342
+ // A resource that exists but has no watcher (the instructions text, a
343
+ // cached result) is NOT a not-found: reporting it as one told clients a
344
+ // URI they can list and read does not exist. Track the two cases apart.
345
+ let knownButNotSubscribable = false;
346
+ for (const contract of resourceContracts) {
347
+ const configured = contract.uri ?? contract.uriTemplate.split('{')[0];
348
+ if (!contract.subscribe) {
349
+ if (configured &&
350
+ checkResourceAllowed({ requestedResource, configuredResource: configured })) {
351
+ knownButNotSubscribable = true;
352
+ }
353
+ continue;
354
+ }
355
+ if (!configured)
356
+ continue;
357
+ if (checkResourceAllowed({
358
+ requestedResource,
359
+ configuredResource: configured,
360
+ })) {
361
+ foundMatch = true;
362
+ const subscribeResult = await contract.subscribe(requestedResource.toString(), notifyUpdated);
363
+ if (subscribeResult === false) {
364
+ // InternalError for want of anything better: ProtocolErrorCode
365
+ // has no resource-limit member, and the message already names
366
+ // the actionable cause.
367
+ throw new ProtocolError(ProtocolErrorCode.InternalError, `Subscription rejected: no watcher attached (watcher limit ${MAX_WATCHERS} reached, or fs.watch failed to start).`);
368
+ }
369
+ break;
370
+ }
371
+ }
372
+ if (!foundMatch) {
373
+ if (knownButNotSubscribable) {
374
+ throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Resource ${requestedResource.toString()} does not support subscriptions; only ${FILESYSTEM_FILE_URI_TEMPLATE} does. Read it again for the current contents.`);
375
+ }
376
+ throw new ResourceNotFoundError(requestedResource.toString(), `Resource not found: ${requestedResource.toString()}`);
377
+ }
378
+ return {};
379
+ });
380
+ server.server.setRequestHandler('resources/unsubscribe', (req) => {
381
+ // Route by URI prefix, mirroring the subscribe handler: a broadcast
382
+ // to every contract works while only one is subscribable, but hands a
383
+ // second subscribable contract someone else's URI the day it appears.
384
+ const requestedResource = resourceUrlFromServerUrl(req.params.uri);
385
+ for (const contract of resourceContracts) {
386
+ if (!contract.unsubscribe)
387
+ continue;
388
+ const configured = contract.uri ?? contract.uriTemplate.split('{')[0];
389
+ if (!configured)
390
+ continue;
391
+ if (checkResourceAllowed({ requestedResource, configuredResource: configured })) {
392
+ contract.unsubscribe(requestedResource.toString());
393
+ }
394
+ }
395
+ return {};
396
+ });
397
+ }
398
+ return {
399
+ dispose() {
400
+ for (const contract of resourceContracts) {
401
+ if (contract.destroy) {
402
+ contract.destroy();
403
+ }
404
+ }
405
+ },
406
+ };
179
407
  }
408
+ //# sourceMappingURL=resources.js.map