@j0hanz/filesystem-mcp 1.6.0 → 1.6.2
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 +16 -0
- package/dist/completions.js +17 -6
- package/dist/lib/constants.d.ts +1 -0
- package/dist/lib/constants.js +2 -1
- package/dist/lib/path-validation.js +7 -1
- package/dist/prompts.js +1 -1
- package/dist/resources/generated-instructions.js +19 -22
- package/dist/resources/tool-catalog.js +2 -2
- package/dist/resources/tool-info.d.ts +1 -0
- package/dist/resources/tool-info.js +23 -1
- package/dist/resources/workflows.js +2 -3
- package/dist/resources.d.ts +1 -0
- package/dist/resources.js +44 -3
- package/dist/server/bootstrap.js +71 -17
- package/dist/server/capabilities.js +9 -1
- package/dist/server/roots-manager.d.ts +2 -0
- package/dist/server/roots-manager.js +24 -6
- package/dist/tools/edit-file.js +2 -1
- package/dist/tools/list-directory.js +8 -3
- package/dist/tools/search-content.js +5 -0
- package/dist/tools/search-files.js +4 -1
- package/dist/tools/shared.d.ts +13 -4
- package/dist/tools/shared.js +20 -23
- package/dist/tools/task-support.js +29 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -565,6 +565,7 @@ Set `FS_CONTEXT_STRIP_STRUCTURED=1` to strip `structuredContent` from tool resul
|
|
|
565
565
|
- **Input limits**: Paths are bounded to 4,096 characters; patterns to 1,000 characters.
|
|
566
566
|
- **Atomic writes**: File writes use an atomic write-then-rename strategy to prevent partial writes.
|
|
567
567
|
- **Docker**: The container runs as a non-root user (`mcp`).
|
|
568
|
+
- **HTTP host binding**: The HTTP transport binds to `127.0.0.1` by default. Setting `FILESYSTEM_MCP_HTTP_HOST=0.0.0.0` binds to all network interfaces and exposes the server externally — only do this behind a trusted reverse proxy with `FILESYSTEM_MCP_AUTH_TOKEN` configured.
|
|
568
569
|
|
|
569
570
|
> [!IMPORTANT]
|
|
570
571
|
> All diagnostic output goes to `stderr`. Tool handlers must never write to `stdout`, as doing so would corrupt the stdio transport.
|
|
@@ -623,6 +624,21 @@ The [Glama](https://glama.ai/mcp/servers/j0hanz/filesystem-mcp) listing requires
|
|
|
623
624
|
docker build -t filesystem-mcp .
|
|
624
625
|
```
|
|
625
626
|
|
|
627
|
+
## HTTP Conformance Notes
|
|
628
|
+
|
|
629
|
+
For Streamable HTTP clients, this server enforces the following behavior:
|
|
630
|
+
|
|
631
|
+
- Session-bound requests must include `MCP-Protocol-Version: 2025-11-25`; missing or unsupported values return `400`.
|
|
632
|
+
- Requests with invalid or expired `mcp-session-id` return `404`.
|
|
633
|
+
- Initialize requests continue to be accepted without a session ID.
|
|
634
|
+
|
|
635
|
+
## Backlog Hardening (Planned)
|
|
636
|
+
|
|
637
|
+
The following hardening items are intentionally tracked as follow-up work:
|
|
638
|
+
|
|
639
|
+
- Add a TTL-evicting task store for long-lived HTTP deployments to bound memory usage.
|
|
640
|
+
- Add an optional per-session roots isolation mode for multi-tenant HTTP deployments.
|
|
641
|
+
|
|
626
642
|
## Troubleshooting
|
|
627
643
|
|
|
628
644
|
**No directories configured**
|
package/dist/completions.js
CHANGED
|
@@ -6,8 +6,18 @@ import { getAllowedDirectories, isPathWithinDirectories, normalizePath, } from '
|
|
|
6
6
|
import { isRecord } from './lib/type-guards.js';
|
|
7
7
|
const MAX_COMPLETION_ITEMS = 100;
|
|
8
8
|
const COMPLETION_RATE_LIMIT_MS = 100;
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// WeakMap keyed by McpServer instance so that each HTTP session gets isolated
|
|
10
|
+
// rate-limit state. In stdio mode there is a single server; in HTTP mode every
|
|
11
|
+
// session creates its own McpServer, so cross-session cache pollution is avoided.
|
|
12
|
+
const completionState = new WeakMap();
|
|
13
|
+
function getCompletionState(server) {
|
|
14
|
+
let state = completionState.get(server);
|
|
15
|
+
if (state === undefined) {
|
|
16
|
+
state = { lastCallMs: new Map(), lastResult: new Map() };
|
|
17
|
+
completionState.set(server, state);
|
|
18
|
+
}
|
|
19
|
+
return state;
|
|
20
|
+
}
|
|
11
21
|
function extractTopicCompletions(instructions) {
|
|
12
22
|
const headers = [];
|
|
13
23
|
for (const line of instructions.split('\n')) {
|
|
@@ -393,9 +403,10 @@ export function registerCompletions(server, instructions = '') {
|
|
|
393
403
|
return { completion: { values: [], total: 0, hasMore: false } };
|
|
394
404
|
}
|
|
395
405
|
const now = Date.now();
|
|
396
|
-
const
|
|
406
|
+
const sessionState = getCompletionState(server);
|
|
407
|
+
const lastCallMs = sessionState.lastCallMs.get(argName) ?? 0;
|
|
397
408
|
if (now - lastCallMs < COMPLETION_RATE_LIMIT_MS) {
|
|
398
|
-
const lastResult =
|
|
409
|
+
const lastResult = sessionState.lastResult.get(argName);
|
|
399
410
|
if (lastResult) {
|
|
400
411
|
return {
|
|
401
412
|
completion: {
|
|
@@ -407,14 +418,14 @@ export function registerCompletions(server, instructions = '') {
|
|
|
407
418
|
}
|
|
408
419
|
return { completion: { values: [], total: 0, hasMore: false } };
|
|
409
420
|
}
|
|
410
|
-
|
|
421
|
+
sessionState.lastCallMs.set(argName, now);
|
|
411
422
|
const contextArguments = extractContextArguments(params.context);
|
|
412
423
|
const { value } = argument;
|
|
413
424
|
const completions = await getPathCompletions(value, {
|
|
414
425
|
argumentName: argName,
|
|
415
426
|
...(contextArguments ? { contextArguments } : {}),
|
|
416
427
|
});
|
|
417
|
-
|
|
428
|
+
sessionState.lastResult.set(argName, completions);
|
|
418
429
|
return {
|
|
419
430
|
completion: {
|
|
420
431
|
values: completions.values,
|
package/dist/lib/constants.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare function parseTrueEnvFlag(value: string | undefined): boolean;
|
|
2
2
|
export declare const DEFAULT_LOG_LEVEL: "debug" | "info" | "notice" | "warning" | "error" | "critical" | "alert" | "emergency";
|
|
3
|
+
export declare const REQUIRED_MCP_PROTOCOL_VERSION = "2025-11-25";
|
|
3
4
|
export declare const PARALLEL_CONCURRENCY: number;
|
|
4
5
|
export declare const MAX_SEARCHABLE_FILE_SIZE: number;
|
|
5
6
|
export declare const MAX_TEXT_FILE_SIZE: number;
|
package/dist/lib/constants.js
CHANGED
|
@@ -69,7 +69,8 @@ function parseEnvLogLevel(envVar, defaultValue) {
|
|
|
69
69
|
console.error(`[WARNING] Invalid ${envVar} value: ${value} (must be ${VALID_LOG_LEVELS.join('|')}). Using default: ${defaultValue}`);
|
|
70
70
|
return defaultValue;
|
|
71
71
|
}
|
|
72
|
-
export const DEFAULT_LOG_LEVEL = parseEnvLogLevel('FILESYSTEM_MCP_LOG_LEVEL', '
|
|
72
|
+
export const DEFAULT_LOG_LEVEL = parseEnvLogLevel('FILESYSTEM_MCP_LOG_LEVEL', 'info');
|
|
73
|
+
export const REQUIRED_MCP_PROTOCOL_VERSION = '2025-11-25';
|
|
73
74
|
// Auto-tuned parallelism based on CPU cores (no env override)
|
|
74
75
|
const BYTES_PER_PARALLEL_TASK = 64 * MIB;
|
|
75
76
|
const BYTES_PER_SEARCH_WORKER = 128 * MIB;
|
|
@@ -108,7 +108,13 @@ function normalizeAllowedDirectories(dirs) {
|
|
|
108
108
|
// Preserve first-seen order while deduping.
|
|
109
109
|
return dedupePreserveOrder(normalized);
|
|
110
110
|
}
|
|
111
|
-
//
|
|
111
|
+
// Process-global singleton state for allowed directory roots.
|
|
112
|
+
//
|
|
113
|
+
// These are set once at startup (via setAllowedDirectoriesResolved) and
|
|
114
|
+
// mutated only through setAllowedDirectoriesState. In stdio mode there is a
|
|
115
|
+
// single MCP session per process, so this is safe. In HTTP mode all HTTP
|
|
116
|
+
// sessions within the same process share one policy — multi-tenant isolation
|
|
117
|
+
// (different roots per session) requires separate server processes.
|
|
112
118
|
let allowedDirectoriesExpanded = [];
|
|
113
119
|
let allowedDirectoriesPrimary = [];
|
|
114
120
|
function setAllowedDirectoriesState(primary, expanded) {
|
package/dist/prompts.js
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { withDefaultIcons } from './tools/shared.js';
|
|
3
3
|
const HELP_PROMPT_NAME = 'get-help';
|
|
4
4
|
const HELP_PROMPT_TITLE = 'Get Help';
|
|
5
|
-
const HELP_PROMPT_DESCRIPTION = '
|
|
5
|
+
const HELP_PROMPT_DESCRIPTION = 'Retrieve the full filesystem-mcp XML usage guide.';
|
|
6
6
|
function filterInstructionsByTopic(instructions, topic) {
|
|
7
7
|
const normalized = topic.trim().toLowerCase();
|
|
8
8
|
if (!normalized)
|
|
@@ -1,46 +1,44 @@
|
|
|
1
1
|
import { buildToolCatalogDetailsOnly } from './tool-catalog.js';
|
|
2
2
|
import { buildCoreContextPack, getSharedConstraints, getToolContracts, } from './tool-info.js';
|
|
3
3
|
import { buildWorkflowGuide } from './workflows.js';
|
|
4
|
-
const INSTRUCTIONS_HEADER =
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
## TOOLS
|
|
4
|
+
const INSTRUCTIONS_HEADER = `<role>
|
|
5
|
+
Expert filesystem agent. Operate ONLY within allowed roots. Always discover before acting — never guess paths.
|
|
6
|
+
</role>
|
|
9
7
|
|
|
8
|
+
<tools_overview>
|
|
10
9
|
| Category | Tools |
|
|
11
10
|
|----------|-------|
|
|
12
11
|
| Navigate | \`roots\`, \`ls\`, \`tree\`, \`find\` |
|
|
13
12
|
| Inspect | \`stat\`, \`stat_many\`, \`grep\`, \`calculate_hash\` |
|
|
14
13
|
| Read | \`read\`, \`read_many\`, \`diff_files\` |
|
|
15
14
|
| Write | \`mkdir\`, \`write\`, \`edit\`, \`mv\`, \`rm\`, \`apply_patch\`, \`search_and_replace\` |
|
|
15
|
+
</tools_overview>
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- \`filesystem-mcp://
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
## TASK PROTOCOL
|
|
17
|
+
<resources>
|
|
18
|
+
- \`filesystem-mcp://result/{id}\`: Large output cache. Call \`resources/read\` immediately if \`resourceUri\` is returned.
|
|
19
|
+
- \`filesystem-mcp://metrics\`: Live per-tool stats.
|
|
20
|
+
</resources>
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
<task_protocol>
|
|
23
|
+
Async execution: provide \`_meta.progressToken\` in \`tools/call\`, poll \`tasks/get\`, call \`tasks/result\`.
|
|
25
24
|
Task-capable: \`find\`, \`tree\`, \`read\`, \`read_many\`, \`stat_many\`, \`grep\`, \`mkdir\`, \`write\`, \`mv\`, \`rm\`, \`calculate_hash\`, \`apply_patch\`, \`search_and_replace\`.
|
|
26
|
-
|
|
25
|
+
</task_protocol>
|
|
27
26
|
`;
|
|
28
|
-
const INSTRUCTIONS_FOOTER =
|
|
29
|
-
## CONSTRAINTS
|
|
30
|
-
|
|
27
|
+
const INSTRUCTIONS_FOOTER = `<constraints>
|
|
31
28
|
${getSharedConstraints()
|
|
32
29
|
.map((c) => `- ${c}`)
|
|
33
30
|
.join('\n')}
|
|
31
|
+
</constraints>
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
<error_handling>
|
|
37
34
|
- \`E_ACCESS_DENIED\` → Call \`roots\`; use allowed path.
|
|
38
35
|
- \`E_NOT_FOUND\` → Call \`ls\`/\`find\`; verify spelling.
|
|
39
36
|
- \`E_TOO_LARGE\` → Use range/head or \`read_many\`.
|
|
40
37
|
- \`E_TIMEOUT\` → Reduce scope or result limits.
|
|
38
|
+
</error_handling>
|
|
41
39
|
`;
|
|
42
40
|
function formatToolSection(tool) {
|
|
43
|
-
const parts = [
|
|
41
|
+
const parts = [`### ${tool.name}\n${tool.description}`];
|
|
44
42
|
if (tool.nuances && tool.nuances.length > 0) {
|
|
45
43
|
parts.push(...tool.nuances.map((n) => `» ${n}`));
|
|
46
44
|
}
|
|
@@ -57,13 +55,12 @@ export function buildServerInstructions() {
|
|
|
57
55
|
'',
|
|
58
56
|
buildToolCatalogDetailsOnly(),
|
|
59
57
|
'',
|
|
60
|
-
'
|
|
61
|
-
'',
|
|
58
|
+
'<tool_reference>',
|
|
62
59
|
toolSections,
|
|
60
|
+
'</tool_reference>',
|
|
63
61
|
'',
|
|
64
62
|
buildWorkflowGuide(),
|
|
65
63
|
'',
|
|
66
|
-
'---',
|
|
67
64
|
INSTRUCTIONS_FOOTER,
|
|
68
65
|
].join('\n');
|
|
69
66
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { buildCoreContextPack } from './tool-info.js';
|
|
2
|
-
const CATALOG_GUIDE =
|
|
3
|
-
|
|
2
|
+
const CATALOG_GUIDE = `<tool_selection_guide>
|
|
4
3
|
## Cross-Tool Data Flow
|
|
5
4
|
|
|
6
5
|
\`\`\`
|
|
@@ -25,6 +24,7 @@ diff_files (patch text) -> apply_patch.patch
|
|
|
25
24
|
- Always generate a patch with \`diff_files\` first.
|
|
26
25
|
- Always use \`dryRun: true\` with \`apply_patch\` to verify changes.
|
|
27
26
|
- \`apply_patch\` works on unified diff format.
|
|
27
|
+
</tool_selection_guide>
|
|
28
28
|
`;
|
|
29
29
|
export function buildToolCatalog() {
|
|
30
30
|
return `${buildCoreContextPack()}\n\n${CATALOG_GUIDE}`;
|
|
@@ -2,3 +2,4 @@ import type { ToolContract } from '../tools/contract.js';
|
|
|
2
2
|
export declare function getToolContracts(): ToolContract[];
|
|
3
3
|
export declare function buildCoreContextPack(): string;
|
|
4
4
|
export declare function getSharedConstraints(): string[];
|
|
5
|
+
export declare function buildToolInfo(name: string): string | undefined;
|
|
@@ -33,7 +33,7 @@ export function buildCoreContextPack() {
|
|
|
33
33
|
const annotations = e.annotations ? ` ${e.annotations.join(' ')}` : '';
|
|
34
34
|
return `| \`${e.name}\` | ${e.description}${annotations} |`;
|
|
35
35
|
});
|
|
36
|
-
return
|
|
36
|
+
return `<core_context>\n| Tool | Purpose |\n|------|---------|\n${rows.join('\n')}\n</core_context>`;
|
|
37
37
|
}
|
|
38
38
|
export function getSharedConstraints() {
|
|
39
39
|
return [
|
|
@@ -43,3 +43,25 @@ export function getSharedConstraints() {
|
|
|
43
43
|
'If a response includes `resourceUri`, call `resources/read` immediately — results expire on process restart.',
|
|
44
44
|
];
|
|
45
45
|
}
|
|
46
|
+
export function buildToolInfo(name) {
|
|
47
|
+
const entry = ENTRIES[name];
|
|
48
|
+
if (!entry)
|
|
49
|
+
return undefined;
|
|
50
|
+
const lines = [`## ${entry.name}`, '', entry.description];
|
|
51
|
+
if (entry.annotations && entry.annotations.length > 0) {
|
|
52
|
+
lines.push('', `**Annotations:** ${entry.annotations.join(', ')}`);
|
|
53
|
+
}
|
|
54
|
+
if (entry.nuances && entry.nuances.length > 0) {
|
|
55
|
+
lines.push('', '**Nuances:**');
|
|
56
|
+
for (const nuance of entry.nuances) {
|
|
57
|
+
lines.push(`- ${nuance}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (entry.gotchas && entry.gotchas.length > 0) {
|
|
61
|
+
lines.push('', '**Gotchas:**');
|
|
62
|
+
for (const gotcha of entry.gotchas) {
|
|
63
|
+
lines.push(`- ${gotcha}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return lines.join('\n');
|
|
67
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export function buildWorkflowGuide() {
|
|
2
|
-
return
|
|
3
|
-
|
|
2
|
+
return `<workflows>
|
|
4
3
|
### A: EXPLORE
|
|
5
4
|
Use when: navigating an unfamiliar directory or reading file content.
|
|
6
5
|
1. \`roots\` (List allowed paths).
|
|
@@ -30,5 +29,5 @@ Use when: applying structured diffs produced by \`diff_files\`.
|
|
|
30
29
|
2. \`apply_patch\` (dryRun: true).
|
|
31
30
|
3. \`apply_patch\` (dryRun: false).
|
|
32
31
|
> **Tip:** Pass \`diff_files\` output directly into \`apply_patch\`.
|
|
33
|
-
|
|
32
|
+
</workflows>`;
|
|
34
33
|
}
|
package/dist/resources.d.ts
CHANGED
|
@@ -5,4 +5,5 @@ export declare function registerInstructionResource(server: McpServer, instructi
|
|
|
5
5
|
export declare function registerToolCatalogResource(server: McpServer, iconInfo?: IconInfo): void;
|
|
6
6
|
export declare function registerWorkflowGuideResource(server: McpServer, iconInfo?: IconInfo): void;
|
|
7
7
|
export declare function registerResultResources(server: McpServer, store: ResourceStore, iconInfo?: IconInfo): void;
|
|
8
|
+
export declare function registerToolInfoResource(server: McpServer, iconInfo?: IconInfo): void;
|
|
8
9
|
export declare function registerMetricsResource(server: McpServer, iconInfo?: IconInfo): void;
|
package/dist/resources.js
CHANGED
|
@@ -2,14 +2,26 @@ import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
2
2
|
import { ErrorCode, McpError } from './lib/errors.js';
|
|
3
3
|
import { globalMetrics } from './lib/observability.js';
|
|
4
4
|
import { buildToolCatalog } from './resources/tool-catalog.js';
|
|
5
|
+
import { buildToolInfo, getToolContracts } from './resources/tool-info.js';
|
|
5
6
|
import { buildWorkflowGuide } from './resources/workflows.js';
|
|
6
7
|
import { withDefaultIcons } from './tools/shared.js';
|
|
7
8
|
const RESULT_TEMPLATE = new ResourceTemplate('filesystem-mcp://result/{id}', {
|
|
8
9
|
list: undefined,
|
|
9
10
|
});
|
|
11
|
+
const TOOL_INFO_TEMPLATE = new ResourceTemplate('internal://tool-info/{name}', {
|
|
12
|
+
list: () => ({
|
|
13
|
+
resources: getToolContracts().map((contract) => ({
|
|
14
|
+
uri: `internal://tool-info/${contract.name}`,
|
|
15
|
+
name: contract.name,
|
|
16
|
+
mimeType: 'text/markdown',
|
|
17
|
+
})),
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
20
|
+
const TOOL_INFO_RESOURCE_NAME = 'filesystem-mcp-tool-info';
|
|
21
|
+
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".';
|
|
10
22
|
const INSTRUCTIONS_RESOURCE_NAME = 'filesystem-mcp-instructions';
|
|
11
23
|
const INSTRUCTIONS_RESOURCE_URI = 'internal://instructions';
|
|
12
|
-
const INSTRUCTIONS_RESOURCE_DESCRIPTION = '
|
|
24
|
+
const INSTRUCTIONS_RESOURCE_DESCRIPTION = 'Comprehensive rules and guidelines for filesystem-mcp usage.';
|
|
13
25
|
const RESULT_RESOURCE_NAME = 'filesystem-mcp-result';
|
|
14
26
|
const RESULT_RESOURCE_DESCRIPTION = 'Ephemeral cached tool output exposed as an MCP resource. Not guaranteed to be listed via resources/list.';
|
|
15
27
|
const METRICS_RESOURCE_NAME = 'filesystem-mcp-metrics';
|
|
@@ -17,10 +29,10 @@ const METRICS_RESOURCE_URI = 'filesystem-mcp://metrics';
|
|
|
17
29
|
const METRICS_RESOURCE_DESCRIPTION = 'Live per-tool call/error/avgDurationMs metrics snapshot.';
|
|
18
30
|
const CATALOG_RESOURCE_NAME = 'filesystem-mcp-catalog';
|
|
19
31
|
const CATALOG_RESOURCE_URI = 'internal://tool-catalog';
|
|
20
|
-
const CATALOG_RESOURCE_DESCRIPTION = '
|
|
32
|
+
const CATALOG_RESOURCE_DESCRIPTION = 'Tool selection guide and data flow map.';
|
|
21
33
|
const WORKFLOW_RESOURCE_NAME = 'filesystem-mcp-workflows';
|
|
22
34
|
const WORKFLOW_RESOURCE_URI = 'internal://workflows';
|
|
23
|
-
const WORKFLOW_RESOURCE_DESCRIPTION = '
|
|
35
|
+
const WORKFLOW_RESOURCE_DESCRIPTION = 'Standard operating procedures for exploration, search, edit, and patch.';
|
|
24
36
|
export function registerInstructionResource(server, instructions, iconInfo) {
|
|
25
37
|
server.registerResource(INSTRUCTIONS_RESOURCE_NAME, INSTRUCTIONS_RESOURCE_URI, withDefaultIcons({
|
|
26
38
|
title: 'Server Instructions',
|
|
@@ -104,6 +116,35 @@ export function registerResultResources(server, store, iconInfo) {
|
|
|
104
116
|
};
|
|
105
117
|
});
|
|
106
118
|
}
|
|
119
|
+
export function registerToolInfoResource(server, iconInfo) {
|
|
120
|
+
server.registerResource(TOOL_INFO_RESOURCE_NAME, TOOL_INFO_TEMPLATE, withDefaultIcons({
|
|
121
|
+
title: 'Tool Info',
|
|
122
|
+
description: TOOL_INFO_RESOURCE_DESCRIPTION,
|
|
123
|
+
mimeType: 'text/markdown',
|
|
124
|
+
annotations: {
|
|
125
|
+
audience: ['assistant'],
|
|
126
|
+
priority: 0.65,
|
|
127
|
+
},
|
|
128
|
+
}, iconInfo), (uri, variables) => {
|
|
129
|
+
const { name } = variables;
|
|
130
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
131
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Tool name is required');
|
|
132
|
+
}
|
|
133
|
+
const content = buildToolInfo(name);
|
|
134
|
+
if (content === undefined) {
|
|
135
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, `Tool not found: ${name}`);
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
contents: [
|
|
139
|
+
{
|
|
140
|
+
uri: uri.href,
|
|
141
|
+
mimeType: 'text/markdown',
|
|
142
|
+
text: content,
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
}
|
|
107
148
|
export function registerMetricsResource(server, iconInfo) {
|
|
108
149
|
server.registerResource(METRICS_RESOURCE_NAME, METRICS_RESOURCE_URI, withDefaultIcons({
|
|
109
150
|
title: 'Tool Metrics',
|
package/dist/server/bootstrap.js
CHANGED
|
@@ -7,12 +7,12 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
7
7
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
8
8
|
import { isInitializeRequest, SetLevelRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
9
9
|
import { registerCompletions } from '../completions.js';
|
|
10
|
-
import { DEFAULT_LOG_LEVEL } from '../lib/constants.js';
|
|
10
|
+
import { DEFAULT_LOG_LEVEL, REQUIRED_MCP_PROTOCOL_VERSION, } from '../lib/constants.js';
|
|
11
11
|
import { formatUnknownErrorMessage } from '../lib/errors.js';
|
|
12
12
|
import { createInMemoryResourceStore } from '../lib/resource-store.js';
|
|
13
13
|
import { pkgInfo } from '../pkg-info.js';
|
|
14
14
|
import { registerGetHelpPrompt } from '../prompts.js';
|
|
15
|
-
import { registerInstructionResource, registerMetricsResource, registerResultResources, registerToolCatalogResource, registerWorkflowGuideResource, } from '../resources.js';
|
|
15
|
+
import { registerInstructionResource, registerMetricsResource, registerResultResources, registerToolCatalogResource, registerToolInfoResource, registerWorkflowGuideResource, } from '../resources.js';
|
|
16
16
|
import { buildServerInstructions } from '../resources/generated-instructions.js';
|
|
17
17
|
import { registerAllTools } from '../tools.js';
|
|
18
18
|
import { withDefaultIcons } from '../tools/shared.js';
|
|
@@ -59,6 +59,7 @@ export async function createServer(options = {}) {
|
|
|
59
59
|
}),
|
|
60
60
|
};
|
|
61
61
|
if (taskToolSupport) {
|
|
62
|
+
// Enabling task tool support requires configuring a task store and message queue on the server config. We use in-memory implementations which are suitable for short-lived stdio sessions. Long-running HTTP servers should replace these with TTL-evicting implementations to avoid unbounded memory growth.
|
|
62
63
|
serverConfig.taskStore = new InMemoryTaskStore();
|
|
63
64
|
serverConfig.taskMessageQueue = new InMemoryTaskMessageQueue();
|
|
64
65
|
}
|
|
@@ -85,6 +86,7 @@ export async function createServer(options = {}) {
|
|
|
85
86
|
registerInstructionResource(server, serverInstructions, localIcon);
|
|
86
87
|
registerToolCatalogResource(server, localIcon);
|
|
87
88
|
registerWorkflowGuideResource(server, localIcon);
|
|
89
|
+
registerToolInfoResource(server, localIcon);
|
|
88
90
|
registerGetHelpPrompt(server, serverInstructions, localIcon);
|
|
89
91
|
registerResultResources(server, resourceStore, localIcon);
|
|
90
92
|
registerMetricsResource(server, localIcon);
|
|
@@ -167,9 +169,6 @@ async function createHttpSession(options, sessions) {
|
|
|
167
169
|
sessions.set(sessionId, { server: mcpServer, transport });
|
|
168
170
|
rootsManager.logMissingDirectoriesIfNeeded(mcpServer);
|
|
169
171
|
},
|
|
170
|
-
onsessionclosed: (sessionId) => {
|
|
171
|
-
sessions.delete(sessionId);
|
|
172
|
-
},
|
|
173
172
|
});
|
|
174
173
|
transport.onclose = () => {
|
|
175
174
|
const { sessionId } = transport;
|
|
@@ -198,11 +197,40 @@ function isAllowedOrigin(origin) {
|
|
|
198
197
|
return true; // Non-browser clients omit Origin.
|
|
199
198
|
return LOCALHOST_ORIGIN_RE.test(origin);
|
|
200
199
|
}
|
|
200
|
+
function getProtocolVersionHeader(req) {
|
|
201
|
+
const rawProtocolVersion = req.headers['mcp-protocol-version'];
|
|
202
|
+
if (typeof rawProtocolVersion === 'string') {
|
|
203
|
+
return rawProtocolVersion;
|
|
204
|
+
}
|
|
205
|
+
if (Array.isArray(rawProtocolVersion)) {
|
|
206
|
+
return rawProtocolVersion.find((value) => value === REQUIRED_MCP_PROTOCOL_VERSION);
|
|
207
|
+
}
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
function ensureProtocolVersionHeader(req, res) {
|
|
211
|
+
const protocolVersion = getProtocolVersionHeader(req);
|
|
212
|
+
if (protocolVersion === REQUIRED_MCP_PROTOCOL_VERSION) {
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
sendJsonRpcError(res, 400, -32000, 'Bad Request: MCP-Protocol-Version header missing or unsupported');
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
function discardRequestBody(req) {
|
|
219
|
+
req.on('error', () => {
|
|
220
|
+
// Best effort drain to avoid corrupting keep-alive pipelines.
|
|
221
|
+
});
|
|
222
|
+
req.resume();
|
|
223
|
+
}
|
|
201
224
|
export async function startHttpServer(port, options) {
|
|
202
225
|
const sessions = new Map();
|
|
203
226
|
async function handleMcpRequest(req, res) {
|
|
204
227
|
const { method } = req;
|
|
205
|
-
const
|
|
228
|
+
const MAX_SESSION_ID_LENGTH = 256;
|
|
229
|
+
const rawSessionId = req.headers['mcp-session-id'];
|
|
230
|
+
const sessionId = typeof rawSessionId === 'string' &&
|
|
231
|
+
rawSessionId.length <= MAX_SESSION_ID_LENGTH
|
|
232
|
+
? rawSessionId
|
|
233
|
+
: undefined;
|
|
206
234
|
const { origin } = req.headers;
|
|
207
235
|
if (!isAllowedOrigin(origin)) {
|
|
208
236
|
sendJsonRpcError(res, 403, -32000, 'Forbidden: disallowed origin');
|
|
@@ -235,33 +263,59 @@ export async function startHttpServer(port, options) {
|
|
|
235
263
|
}
|
|
236
264
|
try {
|
|
237
265
|
if (method === 'POST') {
|
|
238
|
-
|
|
239
|
-
|
|
266
|
+
if (sessionId) {
|
|
267
|
+
if (!sessions.has(sessionId)) {
|
|
268
|
+
sendJsonRpcError(res, 404, -32000, 'Session not found');
|
|
269
|
+
discardRequestBody(req);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (!ensureProtocolVersionHeader(req, res)) {
|
|
273
|
+
discardRequestBody(req);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const body = await readRequestBody(req);
|
|
240
277
|
const session = sessions.get(sessionId);
|
|
241
278
|
if (session) {
|
|
242
279
|
await session.transport.handleRequest(req, res, body);
|
|
243
280
|
}
|
|
281
|
+
else {
|
|
282
|
+
sendJsonRpcError(res, 404, -32000, 'Session not found');
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
244
285
|
}
|
|
245
|
-
|
|
286
|
+
const body = await readRequestBody(req);
|
|
287
|
+
if (isInitializeRequest(body)) {
|
|
288
|
+
const maxSessions = parseInt(process.env['FILESYSTEM_MCP_MAX_HTTP_SESSIONS'] ?? '', 10) || 100;
|
|
289
|
+
if (sessions.size >= maxSessions) {
|
|
290
|
+
sendJsonRpcError(res, 503, -32000, 'Too many sessions');
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
246
293
|
const { transport } = await createHttpSession(options, sessions);
|
|
247
294
|
await transport.handleRequest(req, res, body);
|
|
295
|
+
return;
|
|
248
296
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
sendJsonRpcError(res, 400, -32000, 'Bad Request: No valid session ID provided');
|
|
254
|
-
}
|
|
297
|
+
sendJsonRpcError(res, 400, -32000, 'Bad Request: No valid session ID provided');
|
|
298
|
+
discardRequestBody(req);
|
|
255
299
|
}
|
|
256
300
|
else if (method === 'GET' || method === 'DELETE') {
|
|
257
|
-
if (!sessionId
|
|
258
|
-
sendJsonRpcError(res, 400, -32000, 'Bad Request:
|
|
301
|
+
if (!sessionId) {
|
|
302
|
+
sendJsonRpcError(res, 400, -32000, 'Bad Request: Missing session ID');
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (!sessions.has(sessionId)) {
|
|
306
|
+
sendJsonRpcError(res, 404, -32000, 'Session not found');
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (!ensureProtocolVersionHeader(req, res)) {
|
|
259
310
|
return;
|
|
260
311
|
}
|
|
261
312
|
const session = sessions.get(sessionId);
|
|
262
313
|
if (session) {
|
|
263
314
|
await session.transport.handleRequest(req, res);
|
|
264
315
|
}
|
|
316
|
+
else {
|
|
317
|
+
sendJsonRpcError(res, 404, -32000, 'Session not found');
|
|
318
|
+
}
|
|
265
319
|
}
|
|
266
320
|
else {
|
|
267
321
|
res.writeHead(405, { Allow: 'GET, POST, DELETE' });
|
|
@@ -5,13 +5,16 @@ function detectTaskToolSupport() {
|
|
|
5
5
|
return cachedTaskToolSupport;
|
|
6
6
|
}
|
|
7
7
|
try {
|
|
8
|
+
// Instantiate a minimal, unconnected probe server to duck-type check for
|
|
9
|
+
// task tool support. The probe has no transport or active connections, so
|
|
10
|
+
// close() only releases in-memory state; fire-and-forget is safe here.
|
|
8
11
|
const probe = new McpServer({
|
|
9
12
|
name: 'filesystem-mcp-capability-probe',
|
|
10
13
|
version: '0.0.0',
|
|
11
14
|
}, { capabilities: { tools: {} } });
|
|
12
15
|
cachedTaskToolSupport =
|
|
13
16
|
typeof probe.experimental.tasks.registerToolTask === 'function';
|
|
14
|
-
|
|
17
|
+
probe.close().catch(() => { });
|
|
15
18
|
}
|
|
16
19
|
catch {
|
|
17
20
|
cachedTaskToolSupport = false;
|
|
@@ -27,6 +30,11 @@ export function buildServerCapabilities(options = {}) {
|
|
|
27
30
|
completions: {},
|
|
28
31
|
};
|
|
29
32
|
if (options.enableTaskToolRequests) {
|
|
33
|
+
// NOTE: enabling task tool requests requires the caller to configure
|
|
34
|
+
// an InMemoryTaskStore and InMemoryTaskMessageQueue on the McpServer.
|
|
35
|
+
// InMemoryTaskStore accumulates completed task records with no TTL eviction —
|
|
36
|
+
// suitable for short-lived stdio sessions. Long-running HTTP servers should
|
|
37
|
+
// replace it with a TTL-evicting store to avoid unbounded memory growth.
|
|
30
38
|
capabilities.tasks = {
|
|
31
39
|
list: {},
|
|
32
40
|
cancel: {},
|
|
@@ -5,6 +5,8 @@ export declare class RootsManager {
|
|
|
5
5
|
private rootsUpdateTimeout;
|
|
6
6
|
private rootDirectories;
|
|
7
7
|
private clientInitialized;
|
|
8
|
+
private updatingRoots;
|
|
9
|
+
private pendingRootsUpdate;
|
|
8
10
|
private readonly options;
|
|
9
11
|
readonly loggingState: LoggingState;
|
|
10
12
|
constructor(options: ServerOptions, loggingState: LoggingState);
|
|
@@ -84,6 +84,10 @@ export class RootsManager {
|
|
|
84
84
|
rootsUpdateTimeout;
|
|
85
85
|
rootDirectories = [];
|
|
86
86
|
clientInitialized = false;
|
|
87
|
+
// Set to true when an update is in progress, to prevent concurrent executions. If a change arrives while true, we queue a single retry after completion to ensure the last-known state is applied. This
|
|
88
|
+
updatingRoots = false;
|
|
89
|
+
// If an update is in progress and a change arrives, we set this flag to ensure we run another update after completion to apply the latest state
|
|
90
|
+
pendingRootsUpdate = false;
|
|
87
91
|
options;
|
|
88
92
|
loggingState;
|
|
89
93
|
constructor(options, loggingState) {
|
|
@@ -151,23 +155,37 @@ export class RootsManager {
|
|
|
151
155
|
logToMcp(server, 'warning', 'No allowed directories specified. Please provide directories as command-line arguments or enable --allow-cwd to use the current working directory.', this.loggingState.minimumLevel);
|
|
152
156
|
}
|
|
153
157
|
async updateRootsFromClient(server) {
|
|
158
|
+
// Guard against concurrent executions: if one is already running, queue a
|
|
159
|
+
// single retry so the last-known state is always applied after completion.
|
|
160
|
+
if (this.updatingRoots) {
|
|
161
|
+
this.pendingRootsUpdate = true;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
this.updatingRoots = true;
|
|
154
165
|
try {
|
|
155
166
|
const clientCapabilities = server.server.getClientCapabilities();
|
|
156
167
|
if (!clientCapabilities?.roots) {
|
|
157
168
|
this.rootDirectories = [];
|
|
158
|
-
return;
|
|
159
169
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
170
|
+
else {
|
|
171
|
+
const rootsResult = await server.server.listRoots(undefined, {
|
|
172
|
+
timeout: ROOTS_TIMEOUT_MS,
|
|
173
|
+
});
|
|
174
|
+
const roots = extractRoots(rootsResult);
|
|
175
|
+
this.rootDirectories = await resolveRootDirectories(roots);
|
|
176
|
+
}
|
|
165
177
|
}
|
|
166
178
|
catch (error) {
|
|
167
179
|
logToMcp(server, 'debug', `[DEBUG] MCP Roots protocol unavailable or failed: ${formatUnknownErrorMessage(error)}`, this.loggingState.minimumLevel);
|
|
168
180
|
}
|
|
169
181
|
finally {
|
|
170
182
|
await this.recomputeAllowedDirectories();
|
|
183
|
+
this.updatingRoots = false;
|
|
184
|
+
// If a change arrived while we were running, apply it now.
|
|
185
|
+
if (this.pendingRootsUpdate) {
|
|
186
|
+
this.pendingRootsUpdate = false;
|
|
187
|
+
void this.updateRootsFromClient(server);
|
|
188
|
+
}
|
|
171
189
|
}
|
|
172
190
|
}
|
|
173
191
|
}
|
package/dist/tools/edit-file.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
+
import RE2 from 're2';
|
|
3
4
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
5
|
import { atomicWriteFile } from '../lib/fs-helpers.js';
|
|
5
6
|
import { validateExistingPath } from '../lib/path-validation.js';
|
|
@@ -35,7 +36,7 @@ function applyEdits(content, edits, ignoreWhitespace) {
|
|
|
35
36
|
for (const edit of edits) {
|
|
36
37
|
if (ignoreWhitespace) {
|
|
37
38
|
const pattern = escapeRegExp(edit.oldText).replace(/\s+/g, '\\s+');
|
|
38
|
-
const regex = new
|
|
39
|
+
const regex = new RE2(pattern);
|
|
39
40
|
const match = regex.exec(newContent);
|
|
40
41
|
if (!match) {
|
|
41
42
|
unmatchedEdits.push(edit.oldText);
|
|
@@ -17,9 +17,10 @@ export const LIST_DIRECTORY_TOOL = {
|
|
|
17
17
|
inputSchema: ListDirectoryInputSchema,
|
|
18
18
|
outputSchema: ListDirectoryOutputSchema,
|
|
19
19
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
20
|
+
taskSupport: 'optional',
|
|
20
21
|
nuances: ['`pattern` enables filtered recursive traversal up to `maxDepth`.'],
|
|
21
22
|
};
|
|
22
|
-
function buildListTextResult(result) {
|
|
23
|
+
function buildListTextResult(result, nextCursor) {
|
|
23
24
|
const { entries, summary, path } = result;
|
|
24
25
|
if (entries.length === 0) {
|
|
25
26
|
if (!summary.entriesScanned || summary.entriesScanned === 0) {
|
|
@@ -45,7 +46,11 @@ function buildListTextResult(result) {
|
|
|
45
46
|
truncated: summary.truncated,
|
|
46
47
|
...(truncatedReason ? { truncatedReason } : {}),
|
|
47
48
|
};
|
|
48
|
-
|
|
49
|
+
let text = joinLines(lines) + formatOperationSummary(summaryOptions);
|
|
50
|
+
if (nextCursor) {
|
|
51
|
+
text += `\n[Next page available. Use cursor: "${nextCursor}"]`;
|
|
52
|
+
}
|
|
53
|
+
return text;
|
|
49
54
|
}
|
|
50
55
|
function buildStructuredListEntry(entry) {
|
|
51
56
|
return {
|
|
@@ -115,7 +120,7 @@ async function handleListDirectory(args, signal) {
|
|
|
115
120
|
? encodeCursor(cursorOffset + displayEntries.length)
|
|
116
121
|
: undefined;
|
|
117
122
|
const displayResult = { ...result, entries: displayEntries };
|
|
118
|
-
return buildToolResponse(buildListTextResult(displayResult), buildStructuredListResult(displayResult, nextCursor));
|
|
123
|
+
return buildToolResponse(buildListTextResult(displayResult, nextCursor), buildStructuredListResult(displayResult, nextCursor));
|
|
119
124
|
}
|
|
120
125
|
export function registerListDirectoryTool(server, options = {}) {
|
|
121
126
|
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
@@ -224,6 +224,11 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
224
224
|
});
|
|
225
225
|
};
|
|
226
226
|
try {
|
|
227
|
+
if (signal) {
|
|
228
|
+
signal.addEventListener('abort', () => {
|
|
229
|
+
console.error('searchContent signal aborted!');
|
|
230
|
+
});
|
|
231
|
+
}
|
|
227
232
|
const result = await handleSearchContent(args, signal, options.resourceStore, progressWithMessage);
|
|
228
233
|
const sc = result.structuredContent;
|
|
229
234
|
const count = sc.ok && sc.totalMatches ? sc.totalMatches : 0;
|
|
@@ -113,7 +113,10 @@ async function handleSearchFiles(args, signal, onProgress) {
|
|
|
113
113
|
textLines.push(` ${entry.path}`);
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
|
-
|
|
116
|
+
let text = joinLines(textLines) + formatOperationSummary(summaryOptions);
|
|
117
|
+
if (nextCursor) {
|
|
118
|
+
text += `\n[Next page available. Use cursor: "${nextCursor}"]`;
|
|
119
|
+
}
|
|
117
120
|
return buildToolResponse(text, structured);
|
|
118
121
|
}
|
|
119
122
|
export function registerSearchFilesTool(server, options = {}) {
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { z } from 'zod';
|
|
|
3
3
|
import type { FileInfo } from '../config.js';
|
|
4
4
|
import { ErrorCode } from '../lib/errors.js';
|
|
5
5
|
import type { ResourceStore } from '../lib/resource-store.js';
|
|
6
|
-
import type { ToolErrorResponseSchema } from '../schemas.js';
|
|
7
6
|
export { type ToolContract } from './contract.js';
|
|
8
7
|
export declare const READ_ONLY_TOOL_ANNOTATIONS: {
|
|
9
8
|
readonly readOnlyHint: true;
|
|
@@ -42,12 +41,13 @@ export declare function buildToolResponse<T>(text: string, structuredContent: T,
|
|
|
42
41
|
content: ContentBlock[];
|
|
43
42
|
structuredContent: T;
|
|
44
43
|
};
|
|
45
|
-
export type ToolResponse<T> = ReturnType<typeof buildToolResponse<T>> &
|
|
46
|
-
|
|
44
|
+
export type ToolResponse<T> = ReturnType<typeof buildToolResponse<T>> & {
|
|
45
|
+
isError?: never;
|
|
46
|
+
} & Record<string, unknown>;
|
|
47
47
|
interface ToolErrorResponse extends Record<string, unknown> {
|
|
48
48
|
content: ContentBlock[];
|
|
49
|
-
structuredContent: ToolErrorStructuredContent;
|
|
50
49
|
isError: true;
|
|
50
|
+
errorCode?: string;
|
|
51
51
|
}
|
|
52
52
|
export type ToolResult<T> = ToolResponse<T> | ToolErrorResponse;
|
|
53
53
|
export declare function withValidatedArgs<Args, Result>(schema: z.ZodType<Args>, handler: (args: Args, extra: ToolExtra) => Promise<ToolResult<Result>>): (args: unknown, extra: ToolExtra) => Promise<ToolResult<Result>>;
|
|
@@ -117,4 +117,13 @@ export declare function wrapToolHandler<Args, Result>(handler: (args: Args, extr
|
|
|
117
117
|
progressMessage?: (args: Args) => string;
|
|
118
118
|
completionMessage?: (args: Args, result: ToolResult<Result>) => string | undefined;
|
|
119
119
|
}): (args: Args, extra?: ToolExtra) => Promise<ToolResult<Result>>;
|
|
120
|
+
/**
|
|
121
|
+
* Returns `pathValue` if non-empty; otherwise resolves to the single allowed
|
|
122
|
+
* directory from module-level state managed by `RootsManager`. Throws when the
|
|
123
|
+
* path is ambiguous (multiple roots) or when no roots are configured.
|
|
124
|
+
*
|
|
125
|
+
* NOTE: Depends on `getAllowedDirectories()` which reads module-level state
|
|
126
|
+
* updated by `RootsManager`. Ensure the server is initialized before calling.
|
|
127
|
+
* See `src/server/roots-manager.ts` for the update lifecycle.
|
|
128
|
+
*/
|
|
120
129
|
export declare function resolvePathOrRoot(pathValue: string | undefined): string;
|
package/dist/tools/shared.js
CHANGED
|
@@ -195,24 +195,10 @@ export async function executeToolWithDiagnostics(options) {
|
|
|
195
195
|
export function buildToolErrorResponse(error, defaultCode, path) {
|
|
196
196
|
const detailed = resolveDetailedError(error, defaultCode, path);
|
|
197
197
|
const text = formatDetailedError(detailed);
|
|
198
|
-
const errorContent = {
|
|
199
|
-
code: detailed.code,
|
|
200
|
-
message: detailed.message,
|
|
201
|
-
};
|
|
202
|
-
if (detailed.path !== undefined) {
|
|
203
|
-
errorContent.path = detailed.path;
|
|
204
|
-
}
|
|
205
|
-
if (detailed.suggestion !== undefined) {
|
|
206
|
-
errorContent.suggestion = detailed.suggestion;
|
|
207
|
-
}
|
|
208
|
-
const structuredContent = {
|
|
209
|
-
ok: false,
|
|
210
|
-
error: errorContent,
|
|
211
|
-
};
|
|
212
198
|
return {
|
|
213
199
|
content: [{ type: 'text', text }],
|
|
214
|
-
structuredContent,
|
|
215
200
|
isError: true,
|
|
201
|
+
errorCode: detailed.code,
|
|
216
202
|
};
|
|
217
203
|
}
|
|
218
204
|
function buildNotInitializedResult() {
|
|
@@ -274,10 +260,11 @@ export function createProgressReporter(extra) {
|
|
|
274
260
|
// out-of-order progress is undefined in the MCP spec.
|
|
275
261
|
if (current <= lastProgress)
|
|
276
262
|
return;
|
|
277
|
-
//
|
|
278
|
-
//
|
|
263
|
+
// Terminal notifications always bypass the rate limit so clients reliably
|
|
264
|
+
// receive the final state even when updates arrive in quick succession.
|
|
265
|
+
const isTerminal = total !== undefined && current >= total;
|
|
279
266
|
const now = Date.now();
|
|
280
|
-
if (now - lastSentMs < PROGRESS_RATE_LIMIT_MS)
|
|
267
|
+
if (!isTerminal && now - lastSentMs < PROGRESS_RATE_LIMIT_MS)
|
|
281
268
|
return;
|
|
282
269
|
lastProgress = current;
|
|
283
270
|
lastSentMs = now;
|
|
@@ -298,11 +285,12 @@ async function withProgress(message, extra, run, getCompletionMessage) {
|
|
|
298
285
|
return run();
|
|
299
286
|
}
|
|
300
287
|
const total = 1;
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
288
|
+
// Emit the start notification only when a progressToken is present; for
|
|
289
|
+
// task-only mode the task status is already 'working' — a zero-progress
|
|
290
|
+
// notification would add unnecessary overhead without client value.
|
|
291
|
+
if (canSendProgress(extra)) {
|
|
292
|
+
await reportProgress(extra, { current: 0, total, message });
|
|
293
|
+
}
|
|
306
294
|
try {
|
|
307
295
|
const result = await run();
|
|
308
296
|
const endMessage = getCompletionMessage?.(result) ?? message;
|
|
@@ -341,6 +329,15 @@ export function wrapToolHandler(handler, options) {
|
|
|
341
329
|
return maybeStripStructuredContentFromResult(result);
|
|
342
330
|
};
|
|
343
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* Returns `pathValue` if non-empty; otherwise resolves to the single allowed
|
|
334
|
+
* directory from module-level state managed by `RootsManager`. Throws when the
|
|
335
|
+
* path is ambiguous (multiple roots) or when no roots are configured.
|
|
336
|
+
*
|
|
337
|
+
* NOTE: Depends on `getAllowedDirectories()` which reads module-level state
|
|
338
|
+
* updated by `RootsManager`. Ensure the server is initialized before calling.
|
|
339
|
+
* See `src/server/roots-manager.ts` for the update lifecycle.
|
|
340
|
+
*/
|
|
344
341
|
export function resolvePathOrRoot(pathValue) {
|
|
345
342
|
if (pathValue && pathValue.trim().length > 0)
|
|
346
343
|
return pathValue;
|
|
@@ -134,14 +134,21 @@ function normalizeCallToolResult(value) {
|
|
|
134
134
|
function getToolResultErrorCode(result) {
|
|
135
135
|
if (!isRecord(result) || result['isError'] !== true)
|
|
136
136
|
return undefined;
|
|
137
|
-
|
|
138
|
-
if (
|
|
137
|
+
// First check for a dedicated errorCode property to avoid regex parsing of the content for structured error results produced by newer code.
|
|
138
|
+
if (typeof result['errorCode'] === 'string')
|
|
139
|
+
return result['errorCode'];
|
|
140
|
+
// Fallback to regex parsing of the human-readable error message for older error results that lack a structured errorCode property.
|
|
141
|
+
const { content } = result;
|
|
142
|
+
if (!Array.isArray(content) || content.length === 0)
|
|
139
143
|
return undefined;
|
|
140
|
-
const
|
|
141
|
-
if (!isRecord(
|
|
144
|
+
const first = content[0];
|
|
145
|
+
if (!isRecord(first) || first['type'] !== 'text')
|
|
142
146
|
return undefined;
|
|
143
|
-
const {
|
|
144
|
-
|
|
147
|
+
const { text } = first;
|
|
148
|
+
if (typeof text !== 'string')
|
|
149
|
+
return undefined;
|
|
150
|
+
const match = /^Error \[([A-Z0-9_]+)\]:/.exec(text);
|
|
151
|
+
return match ? match[1] : undefined;
|
|
145
152
|
}
|
|
146
153
|
function isCancelledToolResult(result) {
|
|
147
154
|
return getToolResultErrorCode(result) === ErrorCode.E_CANCELLED;
|
|
@@ -222,7 +229,7 @@ function getTaskId(extra) {
|
|
|
222
229
|
return extra.taskId;
|
|
223
230
|
}
|
|
224
231
|
function isErrorResult(result) {
|
|
225
|
-
return 'isError' in result && result.isError
|
|
232
|
+
return 'isError' in result && result.isError;
|
|
226
233
|
}
|
|
227
234
|
// Strips structuredContent from a tool result if present, without modifying the original object. This is used when storing error results as 'completed' to prevent client-side output schema validation errors, while still allowing the human-readable error message in content[0].text to be returned to clients.
|
|
228
235
|
function withoutStructuredContent(result) {
|
|
@@ -290,6 +297,21 @@ async function runTaskInBackground(run, args, extra, taskStore, taskId, toolName
|
|
|
290
297
|
}
|
|
291
298
|
catch (innerError) {
|
|
292
299
|
console.error(`Failed to store task failure result for task ${taskId}:`, innerError);
|
|
300
|
+
// If storing the failure result also fails, there's not much we can do. The task will remain in 'working' status until it expires, which is not ideal but at least prevents clients from receiving incorrect results or hanging indefinitely waiting for a result that will never arrive. We log the error to aid debugging, and we attempt to notify the client of the failure if possible, but we don't want to throw further errors that could crash the server or cause cascading failures.
|
|
301
|
+
const syntheticTask = {
|
|
302
|
+
taskId,
|
|
303
|
+
status: 'failed',
|
|
304
|
+
ttl: null,
|
|
305
|
+
createdAt: new Date().toISOString(),
|
|
306
|
+
lastUpdatedAt: new Date().toISOString(),
|
|
307
|
+
};
|
|
308
|
+
const { sendNotification } = extra;
|
|
309
|
+
if (typeof sendNotification === 'function') {
|
|
310
|
+
void sendNotification({
|
|
311
|
+
method: TASK_STATUS_NOTIFICATION_METHOD,
|
|
312
|
+
params: buildTaskStatusNotificationParams(syntheticTask),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
293
315
|
}
|
|
294
316
|
}
|
|
295
317
|
}
|