@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.
- package/README.md +333 -838
- package/dist/cli-help.d.ts +3 -0
- package/dist/cli-help.d.ts.map +1 -0
- package/dist/cli-help.js +169 -0
- package/dist/cli-help.js.map +1 -0
- package/dist/cli.d.ts +28 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +168 -123
- package/dist/cli.js.map +1 -0
- package/dist/core/concurrency.d.ts +70 -0
- package/dist/core/concurrency.d.ts.map +1 -0
- package/dist/core/concurrency.js +173 -0
- package/dist/core/concurrency.js.map +1 -0
- package/dist/core/config.d.ts +19 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +11 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/cursor.d.ts +23 -0
- package/dist/core/cursor.d.ts.map +1 -0
- package/dist/core/cursor.js +62 -0
- package/dist/core/cursor.js.map +1 -0
- package/dist/core/errors.d.ts +96 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +356 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/file-uri.d.ts +24 -0
- package/dist/core/file-uri.d.ts.map +1 -0
- package/dist/core/file-uri.js +52 -0
- package/dist/core/file-uri.js.map +1 -0
- package/dist/core/fmt.d.ts +38 -0
- package/dist/core/fmt.d.ts.map +1 -0
- package/dist/core/fmt.js +109 -0
- package/dist/core/fmt.js.map +1 -0
- package/dist/core/fs.d.ts +108 -0
- package/dist/core/fs.d.ts.map +1 -0
- package/dist/core/fs.js +243 -0
- package/dist/core/fs.js.map +1 -0
- package/dist/core/glob.d.ts +41 -0
- package/dist/core/glob.d.ts.map +1 -0
- package/dist/core/glob.js +399 -0
- package/dist/core/glob.js.map +1 -0
- package/dist/core/input-required.d.ts +147 -0
- package/dist/core/input-required.d.ts.map +1 -0
- package/dist/core/input-required.js +261 -0
- package/dist/core/input-required.js.map +1 -0
- package/dist/core/mime.d.ts +15 -0
- package/dist/core/mime.d.ts.map +1 -0
- package/dist/core/mime.js +188 -0
- package/dist/core/mime.js.map +1 -0
- package/dist/core/observability.d.ts +10 -0
- package/dist/core/observability.d.ts.map +1 -0
- package/dist/core/observability.js +76 -0
- package/dist/core/observability.js.map +1 -0
- package/dist/core/page-store.d.ts +41 -0
- package/dist/core/page-store.d.ts.map +1 -0
- package/dist/core/page-store.js +74 -0
- package/dist/core/page-store.js.map +1 -0
- package/dist/core/path-completer.d.ts +10 -0
- package/dist/core/path-completer.d.ts.map +1 -0
- package/dist/core/path-completer.js +227 -0
- package/dist/core/path-completer.js.map +1 -0
- package/dist/core/path-discovery.d.ts +8 -0
- package/dist/core/path-discovery.d.ts.map +1 -0
- package/dist/core/path-discovery.js +105 -0
- package/dist/core/path-discovery.js.map +1 -0
- package/dist/core/path-utils.d.ts +9 -0
- package/dist/core/path-utils.d.ts.map +1 -0
- package/dist/core/path-utils.js +121 -0
- package/dist/core/path-utils.js.map +1 -0
- package/dist/core/path.d.ts +148 -0
- package/dist/core/path.d.ts.map +1 -0
- package/dist/core/path.js +674 -0
- package/dist/core/path.js.map +1 -0
- package/dist/core/primitives.d.ts +23 -0
- package/dist/core/primitives.d.ts.map +1 -0
- package/dist/core/primitives.js +54 -0
- package/dist/core/primitives.js.map +1 -0
- package/dist/core/read.d.ts +74 -0
- package/dist/core/read.d.ts.map +1 -0
- package/dist/core/read.js +562 -0
- package/dist/core/read.js.map +1 -0
- package/dist/core/schema.d.ts +139 -0
- package/dist/core/schema.d.ts.map +1 -0
- package/dist/core/schema.js +345 -0
- package/dist/core/schema.js.map +1 -0
- package/dist/core/search.d.ts +100 -0
- package/dist/core/search.d.ts.map +1 -0
- package/dist/core/search.js +248 -0
- package/dist/core/search.js.map +1 -0
- package/dist/core/sensitive.d.ts +6 -0
- package/dist/core/sensitive.d.ts.map +1 -0
- package/dist/core/sensitive.js +142 -0
- package/dist/core/sensitive.js.map +1 -0
- package/dist/core/store.d.ts +57 -0
- package/dist/core/store.d.ts.map +1 -0
- package/dist/core/store.js +162 -0
- package/dist/core/store.js.map +1 -0
- package/dist/core/util.d.ts +26 -0
- package/dist/core/util.d.ts.map +1 -0
- package/dist/core/util.js +66 -0
- package/dist/core/util.js.map +1 -0
- package/dist/core/watcher-registry.d.ts +68 -0
- package/dist/core/watcher-registry.d.ts.map +1 -0
- package/dist/core/watcher-registry.js +312 -0
- package/dist/core/watcher-registry.js.map +1 -0
- package/dist/http-policy.d.ts +121 -0
- package/dist/http-policy.d.ts.map +1 -0
- package/dist/http-policy.js +340 -0
- package/dist/http-policy.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +77 -41
- package/dist/index.js.map +1 -0
- package/dist/instructions.d.ts +12 -0
- package/dist/instructions.d.ts.map +1 -0
- package/dist/instructions.js +71 -0
- package/dist/instructions.js.map +1 -0
- package/dist/prompts.d.ts +8 -6
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +74 -168
- package/dist/prompts.js.map +1 -0
- package/dist/resources.d.ts +73 -9
- package/dist/resources.d.ts.map +1 -0
- package/dist/resources.js +392 -163
- package/dist/resources.js.map +1 -0
- package/dist/server.d.ts +62 -1
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +139 -1
- package/dist/server.js.map +1 -0
- package/dist/tools/batch.d.ts +89 -0
- package/dist/tools/batch.d.ts.map +1 -0
- package/dist/tools/batch.js +157 -0
- package/dist/tools/batch.js.map +1 -0
- package/dist/tools/create.d.ts +2 -0
- package/dist/tools/create.d.ts.map +1 -0
- package/dist/tools/create.js +142 -0
- package/dist/tools/create.js.map +1 -0
- package/dist/tools/define.d.ts +110 -0
- package/dist/tools/define.d.ts.map +1 -0
- package/dist/tools/define.js +394 -0
- package/dist/tools/define.js.map +1 -0
- package/dist/tools/delete-file.d.ts +2 -4
- package/dist/tools/delete-file.d.ts.map +1 -0
- package/dist/tools/delete-file.js +330 -87
- package/dist/tools/delete-file.js.map +1 -0
- package/dist/tools/diff.d.ts +2 -0
- package/dist/tools/diff.d.ts.map +1 -0
- package/dist/tools/diff.js +94 -0
- package/dist/tools/diff.js.map +1 -0
- package/dist/tools/edit.d.ts +2 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +442 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/index.d.ts +26 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +52 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/list.d.ts +2 -0
- package/dist/tools/list.d.ts.map +1 -0
- package/dist/tools/list.js +291 -0
- package/dist/tools/list.js.map +1 -0
- package/dist/tools/move.d.ts +2 -0
- package/dist/tools/move.d.ts.map +1 -0
- package/dist/tools/move.js +321 -0
- package/dist/tools/move.js.map +1 -0
- package/dist/tools/patch.d.ts +2 -0
- package/dist/tools/patch.d.ts.map +1 -0
- package/dist/tools/patch.js +155 -0
- package/dist/tools/patch.js.map +1 -0
- package/dist/tools/progress.d.ts +52 -0
- package/dist/tools/progress.d.ts.map +1 -0
- package/dist/tools/progress.js +172 -0
- package/dist/tools/progress.js.map +1 -0
- package/dist/tools/read.d.ts +2 -4
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +398 -152
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/replace-in-files.d.ts +2 -4
- package/dist/tools/replace-in-files.d.ts.map +1 -0
- package/dist/tools/replace-in-files.js +435 -264
- package/dist/tools/replace-in-files.js.map +1 -0
- package/dist/tools/roots.d.ts +2 -4
- package/dist/tools/roots.d.ts.map +1 -0
- package/dist/tools/roots.js +29 -52
- package/dist/tools/roots.js.map +1 -0
- package/dist/tools/search-content.d.ts +2 -4
- package/dist/tools/search-content.d.ts.map +1 -0
- package/dist/tools/search-content.js +262 -228
- package/dist/tools/search-content.js.map +1 -0
- package/dist/tools/search-files.d.ts +2 -4
- package/dist/tools/search-files.d.ts.map +1 -0
- package/dist/tools/search-files.js +162 -161
- package/dist/tools/search-files.js.map +1 -0
- package/dist/tools/stat.d.ts +2 -4
- package/dist/tools/stat.d.ts.map +1 -0
- package/dist/tools/stat.js +202 -62
- package/dist/tools/stat.js.map +1 -0
- package/dist/transport/http.d.ts +5 -0
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/http.js +276 -0
- package/dist/transport/http.js.map +1 -0
- package/dist/transport/shared.d.ts +45 -0
- package/dist/transport/shared.d.ts.map +1 -0
- package/dist/transport/shared.js +65 -0
- package/dist/transport/shared.js.map +1 -0
- package/dist/transport/stdio.d.ts +32 -0
- package/dist/transport/stdio.d.ts.map +1 -0
- package/dist/transport/stdio.js +279 -0
- package/dist/transport/stdio.js.map +1 -0
- package/dist/transport.d.ts +5 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +4 -0
- package/dist/transport.js.map +1 -0
- package/package.json +37 -46
- package/dist/assets/logo.svg +0 -35
- package/dist/completions.d.ts +0 -2
- package/dist/completions.js +0 -572
- package/dist/config.d.ts +0 -119
- package/dist/config.js +0 -31
- package/dist/lib/abort.d.ts +0 -7
- package/dist/lib/abort.js +0 -81
- package/dist/lib/constants.d.ts +0 -42
- package/dist/lib/constants.js +0 -313
- package/dist/lib/errors.d.ts +0 -28
- package/dist/lib/errors.js +0 -301
- package/dist/lib/file-operations/core.d.ts +0 -53
- package/dist/lib/file-operations/core.js +0 -144
- package/dist/lib/file-operations/metadata.d.ts +0 -77
- package/dist/lib/file-operations/metadata.js +0 -878
- package/dist/lib/file-operations/search.d.ts +0 -46
- package/dist/lib/file-operations/search.js +0 -1194
- package/dist/lib/file-operations/traversal.d.ts +0 -40
- package/dist/lib/file-operations/traversal.js +0 -310
- package/dist/lib/fs-helpers.d.ts +0 -46
- package/dist/lib/fs-helpers.js +0 -606
- package/dist/lib/globs.d.ts +0 -2
- package/dist/lib/globs.js +0 -19
- package/dist/lib/logger.d.ts +0 -27
- package/dist/lib/logger.js +0 -91
- package/dist/lib/observability.d.ts +0 -33
- package/dist/lib/observability.js +0 -373
- package/dist/lib/paths.d.ts +0 -35
- package/dist/lib/paths.js +0 -634
- package/dist/lib/resource-store.d.ts +0 -28
- package/dist/lib/resource-store.js +0 -195
- package/dist/lib/utils.d.ts +0 -7
- package/dist/lib/utils.js +0 -37
- package/dist/lib/zod-codecs.d.ts +0 -2
- package/dist/lib/zod-codecs.js +0 -18
- package/dist/pkg-info.d.ts +0 -7
- package/dist/pkg-info.js +0 -9
- package/dist/resources/generated-instructions.d.ts +0 -1
- package/dist/resources/generated-instructions.js +0 -105
- package/dist/resources/tool-catalog.d.ts +0 -2
- package/dist/resources/tool-catalog.js +0 -78
- package/dist/resources/tool-info.d.ts +0 -9
- package/dist/resources/tool-info.js +0 -206
- package/dist/resources/workflows.d.ts +0 -1
- package/dist/resources/workflows.js +0 -72
- package/dist/schemas.d.ts +0 -505
- package/dist/schemas.js +0 -676
- package/dist/server/bootstrap.d.ts +0 -6
- package/dist/server/bootstrap.js +0 -563
- package/dist/server/event-store.d.ts +0 -18
- package/dist/server/event-store.js +0 -71
- package/dist/server/roots-manager.d.ts +0 -28
- package/dist/server/roots-manager.js +0 -227
- package/dist/server/task-store.d.ts +0 -10
- package/dist/server/task-store.js +0 -91
- package/dist/tools/apply-patch.d.ts +0 -4
- package/dist/tools/apply-patch.js +0 -235
- package/dist/tools/calculate-hash.d.ts +0 -4
- package/dist/tools/calculate-hash.js +0 -187
- package/dist/tools/contract.d.ts +0 -50
- package/dist/tools/contract.js +0 -1
- package/dist/tools/create-directory.d.ts +0 -4
- package/dist/tools/create-directory.js +0 -68
- package/dist/tools/diff-files.d.ts +0 -4
- package/dist/tools/diff-files.js +0 -136
- package/dist/tools/edit-file.d.ts +0 -4
- package/dist/tools/edit-file.js +0 -256
- package/dist/tools/icons.d.ts +0 -15
- package/dist/tools/icons.js +0 -24
- package/dist/tools/list-directory.d.ts +0 -4
- package/dist/tools/list-directory.js +0 -218
- package/dist/tools/move-file.d.ts +0 -4
- package/dist/tools/move-file.js +0 -178
- package/dist/tools/read-multiple.d.ts +0 -4
- package/dist/tools/read-multiple.js +0 -207
- package/dist/tools/shared.d.ts +0 -172
- package/dist/tools/shared.js +0 -599
- package/dist/tools/stat-many.d.ts +0 -4
- package/dist/tools/stat-many.js +0 -104
- package/dist/tools/task-support.d.ts +0 -29
- package/dist/tools/task-support.js +0 -472
- package/dist/tools/tree.d.ts +0 -4
- package/dist/tools/tree.js +0 -102
- package/dist/tools/write-file.d.ts +0 -4
- package/dist/tools/write-file.js +0 -60
- package/dist/tools.d.ts +0 -5
- 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 {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
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:
|
|
28
|
+
description: INSTRUCTIONS_SUMMARY,
|
|
41
29
|
mimeType: 'text/markdown',
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
}
|
|
47
|
-
contents: [
|
|
48
|
-
{
|
|
49
|
-
uri: uri.href,
|
|
50
|
-
mimeType: 'text/markdown',
|
|
51
|
-
text: instructions,
|
|
52
|
-
},
|
|
53
|
-
],
|
|
54
|
-
}));
|
|
44
|
+
};
|
|
55
45
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
95
|
-
|
|
186
|
+
// ═══════════════════════════════════════════════════════════════
|
|
187
|
+
// result
|
|
188
|
+
// ═══════════════════════════════════════════════════════════════
|
|
189
|
+
function createResultResource(options) {
|
|
190
|
+
return {
|
|
191
|
+
name: 'filesystem-mcp-result',
|
|
96
192
|
title: 'Cached Tool Result',
|
|
97
|
-
description:
|
|
98
|
-
mimeType: '
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
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
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
contents: [
|
|
140
|
-
{
|
|
141
|
-
uri: uri.href,
|
|
142
|
-
mimeType: 'text/markdown',
|
|
143
|
-
text: content,
|
|
144
|
-
},
|
|
145
|
-
],
|
|
146
|
-
};
|
|
147
|
-
});
|
|
278
|
+
};
|
|
148
279
|
}
|
|
149
|
-
export function
|
|
150
|
-
server
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|