@j0hanz/filesystem-mcp 1.5.4 → 1.6.1
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/errors.d.ts +1 -0
- package/dist/lib/errors.js +5 -0
- package/dist/lib/fs-helpers.js +1 -6
- package/dist/lib/path-validation.js +7 -1
- package/dist/resources/tool-info.d.ts +1 -0
- package/dist/resources/tool-info.js +22 -0
- package/dist/resources.d.ts +1 -0
- package/dist/resources.js +41 -0
- package/dist/schemas.d.ts +20 -8
- package/dist/schemas.js +54 -10
- package/dist/server/bootstrap.js +73 -14
- package/dist/server/capabilities.js +5 -0
- package/dist/tools/create-directory.d.ts +4 -1
- package/dist/tools/create-directory.js +21 -14
- package/dist/tools/edit-file.d.ts +4 -1
- package/dist/tools/edit-file.js +47 -17
- package/dist/tools/list-directory.js +8 -3
- package/dist/tools/move-file.d.ts +4 -1
- package/dist/tools/move-file.js +93 -22
- package/dist/tools/replace-in-files.d.ts +7 -1
- package/dist/tools/replace-in-files.js +6 -3
- package/dist/tools/search-content.js +5 -0
- package/dist/tools/search-files.js +4 -1
- package/dist/tools/shared.d.ts +3 -4
- package/dist/tools/shared.js +5 -22
- package/dist/tools/task-support.js +10 -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;
|
package/dist/lib/errors.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ interface DetailedError {
|
|
|
9
9
|
}
|
|
10
10
|
export declare function isNodeError(error: unknown): error is NodeJS.ErrnoException;
|
|
11
11
|
export declare function formatUnknownErrorMessage(error: unknown): string;
|
|
12
|
+
export declare function normalizeUnknownError(error: unknown): Error;
|
|
12
13
|
export declare function isAbortError(error: unknown): boolean;
|
|
13
14
|
export declare function isTimeoutLikeError(error: unknown): boolean;
|
|
14
15
|
export declare class McpError extends Error {
|
package/dist/lib/errors.js
CHANGED
|
@@ -94,6 +94,11 @@ export function formatUnknownErrorMessage(error) {
|
|
|
94
94
|
return String(error);
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
+
export function normalizeUnknownError(error) {
|
|
98
|
+
return error instanceof Error
|
|
99
|
+
? error
|
|
100
|
+
: new Error(formatUnknownErrorMessage(error));
|
|
101
|
+
}
|
|
97
102
|
const NODE_ERROR_CODE_MAP = {
|
|
98
103
|
ENOENT: ErrorCode.E_NOT_FOUND,
|
|
99
104
|
EACCES: ErrorCode.E_PERMISSION_DENIED,
|
package/dist/lib/fs-helpers.js
CHANGED
|
@@ -5,7 +5,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
5
5
|
import { Writable } from 'node:stream';
|
|
6
6
|
import { pipeline } from 'node:stream/promises';
|
|
7
7
|
import { BINARY_CHECK_BUFFER_SIZE, KNOWN_BINARY_EXTENSIONS, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from './constants.js';
|
|
8
|
-
import { ErrorCode,
|
|
8
|
+
import { ErrorCode, McpError, normalizeUnknownError } from './errors.js';
|
|
9
9
|
import { assertAllowedFileAccess } from './path-policy.js';
|
|
10
10
|
import { validateExistingPath } from './path-validation.js';
|
|
11
11
|
function createAbortError(message = 'Operation aborted') {
|
|
@@ -20,11 +20,6 @@ function normalizeAbortReason(reason, message) {
|
|
|
20
20
|
function isFiniteNumber(value) {
|
|
21
21
|
return typeof value === 'number' && Number.isFinite(value);
|
|
22
22
|
}
|
|
23
|
-
function normalizeUnknownError(error) {
|
|
24
|
-
return error instanceof Error
|
|
25
|
-
? error
|
|
26
|
-
: new Error(formatUnknownErrorMessage(error));
|
|
27
|
-
}
|
|
28
23
|
export function assertNotAborted(signal, message) {
|
|
29
24
|
if (!signal)
|
|
30
25
|
return;
|
|
@@ -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) {
|
|
@@ -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;
|
|
@@ -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
|
+
}
|
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,11 +2,23 @@ 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
24
|
const INSTRUCTIONS_RESOURCE_DESCRIPTION = 'Guidance for using the filesystem-mcp MCP tools effectively.';
|
|
@@ -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/schemas.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export declare const ToolErrorResponseSchema: z.ZodObject<{
|
|
|
33
33
|
suggestion: z.ZodOptional<z.ZodString>;
|
|
34
34
|
}, z.core.$strict>;
|
|
35
35
|
}, z.core.$strict>;
|
|
36
|
+
declare const HeadLinesSchema: z.ZodOptional<z.ZodInt>;
|
|
37
|
+
declare const LineNumberSchema: z.ZodNumber;
|
|
36
38
|
export declare const ListDirectoryInputSchema: z.ZodObject<{
|
|
37
39
|
path: z.ZodOptional<z.ZodString>;
|
|
38
40
|
includeHidden: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
@@ -85,16 +87,16 @@ export declare const SearchContentInputSchema: z.ZodObject<{
|
|
|
85
87
|
includeIgnored: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
86
88
|
}, z.core.$strict>;
|
|
87
89
|
export declare const ReadFileInputSchema: z.ZodObject<{
|
|
90
|
+
head: typeof HeadLinesSchema;
|
|
91
|
+
startLine: z.ZodOptional<typeof LineNumberSchema>;
|
|
92
|
+
endLine: z.ZodOptional<typeof LineNumberSchema>;
|
|
88
93
|
path: z.ZodString;
|
|
89
|
-
head: z.ZodOptional<z.ZodInt>;
|
|
90
|
-
startLine: z.ZodOptional<z.ZodNumber>;
|
|
91
|
-
endLine: z.ZodOptional<z.ZodNumber>;
|
|
92
94
|
}, z.core.$strict>;
|
|
93
95
|
export declare const ReadMultipleFilesInputSchema: z.ZodObject<{
|
|
96
|
+
head: typeof HeadLinesSchema;
|
|
97
|
+
startLine: z.ZodOptional<typeof LineNumberSchema>;
|
|
98
|
+
endLine: z.ZodOptional<typeof LineNumberSchema>;
|
|
94
99
|
paths: z.ZodArray<z.ZodString>;
|
|
95
|
-
head: z.ZodOptional<z.ZodInt>;
|
|
96
|
-
startLine: z.ZodOptional<z.ZodNumber>;
|
|
97
|
-
endLine: z.ZodOptional<z.ZodNumber>;
|
|
98
100
|
}, z.core.$strict>;
|
|
99
101
|
export declare const GetFileInfoInputSchema: z.ZodObject<{
|
|
100
102
|
path: z.ZodString;
|
|
@@ -472,11 +474,13 @@ export declare const GetMultipleFileInfoOutputSchema: z.ZodObject<{
|
|
|
472
474
|
}, z.core.$strict>>;
|
|
473
475
|
}, z.core.$strict>;
|
|
474
476
|
export declare const CreateDirectoryInputSchema: z.ZodObject<{
|
|
475
|
-
path: z.ZodString
|
|
477
|
+
path: z.ZodOptional<z.ZodString>;
|
|
478
|
+
paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
476
479
|
}, z.core.$strict>;
|
|
477
480
|
export declare const CreateDirectoryOutputSchema: z.ZodObject<{
|
|
478
481
|
ok: z.ZodBoolean;
|
|
479
482
|
path: z.ZodOptional<z.ZodString>;
|
|
483
|
+
paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
480
484
|
error: z.ZodOptional<z.ZodObject<{
|
|
481
485
|
code: z.ZodEnum<{
|
|
482
486
|
readonly E_ACCESS_DENIED: "E_ACCESS_DENIED";
|
|
@@ -532,6 +536,7 @@ export declare const EditFileInputSchema: z.ZodObject<{
|
|
|
532
536
|
newText: z.ZodString;
|
|
533
537
|
}, z.core.$strict>>;
|
|
534
538
|
dryRun: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
539
|
+
ignoreWhitespace: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
535
540
|
}, z.core.$strict>;
|
|
536
541
|
export declare const EditFileOutputSchema: z.ZodObject<{
|
|
537
542
|
ok: z.ZodBoolean;
|
|
@@ -560,13 +565,19 @@ export declare const EditFileOutputSchema: z.ZodObject<{
|
|
|
560
565
|
}, z.core.$strict>>;
|
|
561
566
|
}, z.core.$strict>;
|
|
562
567
|
export declare const MoveFileInputSchema: z.ZodObject<{
|
|
563
|
-
source: z.ZodString
|
|
568
|
+
source: z.ZodOptional<z.ZodString>;
|
|
569
|
+
sources: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
564
570
|
destination: z.ZodString;
|
|
565
571
|
}, z.core.$strict>;
|
|
566
572
|
export declare const MoveFileOutputSchema: z.ZodObject<{
|
|
567
573
|
ok: z.ZodBoolean;
|
|
568
574
|
source: z.ZodOptional<z.ZodString>;
|
|
575
|
+
sources: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
569
576
|
destination: z.ZodOptional<z.ZodString>;
|
|
577
|
+
failed: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
578
|
+
source: z.ZodString;
|
|
579
|
+
error: z.ZodString;
|
|
580
|
+
}, z.core.$strict>>>;
|
|
570
581
|
error: z.ZodOptional<z.ZodObject<{
|
|
571
582
|
code: z.ZodEnum<{
|
|
572
583
|
readonly E_ACCESS_DENIED: "E_ACCESS_DENIED";
|
|
@@ -717,6 +728,7 @@ export declare const SearchAndReplaceInputSchema: z.ZodObject<{
|
|
|
717
728
|
dryRun: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
718
729
|
includeHidden: z.ZodOptional<z.ZodBoolean>;
|
|
719
730
|
includeIgnored: z.ZodOptional<z.ZodBoolean>;
|
|
731
|
+
returnDiff: z.ZodOptional<z.ZodBoolean>;
|
|
720
732
|
}, z.core.$strict>;
|
|
721
733
|
export declare const SearchAndReplaceOutputSchema: z.ZodObject<{
|
|
722
734
|
ok: z.ZodBoolean;
|
package/dist/schemas.js
CHANGED
|
@@ -81,6 +81,13 @@ const validateReadRange = (value, ctx) => {
|
|
|
81
81
|
addReadRangeIssue(ctx, 'endLine', "'endLine' must be >= 'startLine'");
|
|
82
82
|
}
|
|
83
83
|
};
|
|
84
|
+
function createReadRangeInputFields(descriptions) {
|
|
85
|
+
return {
|
|
86
|
+
head: HeadLinesSchema.describe(descriptions.head),
|
|
87
|
+
startLine: LineNumberSchema.optional().describe(descriptions.startLine),
|
|
88
|
+
endLine: LineNumberSchema.optional().describe(descriptions.endLine),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
84
91
|
const FileInfoSchema = z.strictObject({
|
|
85
92
|
name: z.string().describe('Name'),
|
|
86
93
|
path: z.string().describe('Absolute path'),
|
|
@@ -280,9 +287,11 @@ export const SearchContentInputSchema = z.strictObject({
|
|
|
280
287
|
export const ReadFileInputSchema = z
|
|
281
288
|
.strictObject({
|
|
282
289
|
path: RequiredPathSchema.describe(DESC_PATH_REQUIRED),
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
290
|
+
...createReadRangeInputFields({
|
|
291
|
+
head: 'Read first N lines (preview)',
|
|
292
|
+
startLine: 'Start line (1-based, inclusive)',
|
|
293
|
+
endLine: 'End line (1-based, inclusive). Requires startLine.',
|
|
294
|
+
}),
|
|
286
295
|
})
|
|
287
296
|
.superRefine(validateReadRange);
|
|
288
297
|
export const ReadMultipleFilesInputSchema = z
|
|
@@ -292,9 +301,11 @@ export const ReadMultipleFilesInputSchema = z
|
|
|
292
301
|
.min(1, 'Min 1 path required')
|
|
293
302
|
.max(100, 'Max 100 files')
|
|
294
303
|
.describe('Files to read. e.g. ["src/index.ts"]'),
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
304
|
+
...createReadRangeInputFields({
|
|
305
|
+
head: 'Read first N lines of each file',
|
|
306
|
+
startLine: 'Start line (1-based, inclusive) per file',
|
|
307
|
+
endLine: 'End line (1-based, inclusive) per file. Requires startLine.',
|
|
308
|
+
}),
|
|
298
309
|
})
|
|
299
310
|
.superRefine(validateReadRange);
|
|
300
311
|
export const GetFileInfoInputSchema = z.strictObject({
|
|
@@ -459,12 +470,22 @@ export const GetMultipleFileInfoOutputSchema = z.strictObject({
|
|
|
459
470
|
summary: OperationSummarySchema.optional(),
|
|
460
471
|
error: ErrorSchema.optional(),
|
|
461
472
|
});
|
|
462
|
-
export const CreateDirectoryInputSchema = z
|
|
463
|
-
|
|
473
|
+
export const CreateDirectoryInputSchema = z
|
|
474
|
+
.strictObject({
|
|
475
|
+
path: RequiredPathSchema.optional().describe(DESC_PATH_REQUIRED),
|
|
476
|
+
paths: z
|
|
477
|
+
.array(RequiredPathSchema)
|
|
478
|
+
.optional()
|
|
479
|
+
.describe('Absolute paths to directories to create'),
|
|
480
|
+
})
|
|
481
|
+
.refine((data) => data.path !== undefined || data.paths !== undefined, {
|
|
482
|
+
message: "Either 'path' or 'paths' must be provided",
|
|
483
|
+
path: ['path'],
|
|
464
484
|
});
|
|
465
485
|
export const CreateDirectoryOutputSchema = z.strictObject({
|
|
466
486
|
ok: z.boolean(),
|
|
467
487
|
path: z.string().optional(),
|
|
488
|
+
paths: z.array(z.string()).optional(),
|
|
468
489
|
error: ErrorSchema.optional(),
|
|
469
490
|
});
|
|
470
491
|
export const WriteFileInputSchema = z.strictObject({
|
|
@@ -495,6 +516,11 @@ export const EditFileInputSchema = z.strictObject({
|
|
|
495
516
|
.optional()
|
|
496
517
|
.default(false)
|
|
497
518
|
.describe('Preview edits without writing. Check unmatchedEdits in the response to verify all oldText values were found.'),
|
|
519
|
+
ignoreWhitespace: z
|
|
520
|
+
.boolean()
|
|
521
|
+
.optional()
|
|
522
|
+
.default(false)
|
|
523
|
+
.describe('Ignore leading/trailing whitespace and treat all whitespace sequences as equivalent when matching oldText.'),
|
|
498
524
|
});
|
|
499
525
|
export const EditFileOutputSchema = z.strictObject({
|
|
500
526
|
ok: z.boolean(),
|
|
@@ -510,14 +536,28 @@ export const EditFileOutputSchema = z.strictObject({
|
|
|
510
536
|
.describe('Edits that could not be applied'),
|
|
511
537
|
error: ErrorSchema.optional(),
|
|
512
538
|
});
|
|
513
|
-
export const MoveFileInputSchema = z
|
|
514
|
-
|
|
539
|
+
export const MoveFileInputSchema = z
|
|
540
|
+
.strictObject({
|
|
541
|
+
source: RequiredPathSchema.optional().describe('Path to move (deprecated: use sources)'),
|
|
542
|
+
sources: z.array(RequiredPathSchema).optional().describe('Paths to move'),
|
|
515
543
|
destination: RequiredPathSchema.describe('New path'),
|
|
544
|
+
})
|
|
545
|
+
.refine((data) => (data.source ?? data.sources) !== undefined, {
|
|
546
|
+
message: "Either 'source' or 'sources' must be provided",
|
|
547
|
+
path: ['source'],
|
|
516
548
|
});
|
|
517
549
|
export const MoveFileOutputSchema = z.strictObject({
|
|
518
550
|
ok: z.boolean(),
|
|
519
551
|
source: z.string().optional(),
|
|
552
|
+
sources: z.array(z.string()).optional(),
|
|
520
553
|
destination: z.string().optional(),
|
|
554
|
+
failed: z
|
|
555
|
+
.array(z.strictObject({
|
|
556
|
+
source: z.string().describe('Source path'),
|
|
557
|
+
error: z.string().describe('Error message'),
|
|
558
|
+
}))
|
|
559
|
+
.optional()
|
|
560
|
+
.describe('List of files that failed to move'),
|
|
521
561
|
error: ErrorSchema.optional(),
|
|
522
562
|
});
|
|
523
563
|
export const DeleteFileInputSchema = z.strictObject({
|
|
@@ -643,6 +683,10 @@ export const SearchAndReplaceInputSchema = z.strictObject({
|
|
|
643
683
|
.boolean()
|
|
644
684
|
.optional()
|
|
645
685
|
.describe('Include files and directories ignored by .gitignore rules (e.g. node_modules, dist). Default: false.'),
|
|
686
|
+
returnDiff: z
|
|
687
|
+
.boolean()
|
|
688
|
+
.optional()
|
|
689
|
+
.describe('Return unified diff of changes even if dryRun is false. Default: false.'),
|
|
646
690
|
});
|
|
647
691
|
export const SearchAndReplaceOutputSchema = z.strictObject({
|
|
648
692
|
ok: z.boolean(),
|
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,9 @@ export async function createServer(options = {}) {
|
|
|
59
59
|
}),
|
|
60
60
|
};
|
|
61
61
|
if (taskToolSupport) {
|
|
62
|
+
// Note: InMemoryTaskStore has no TTL — tasks accumulate for the process
|
|
63
|
+
// lifetime. In HTTP mode this may grow unboundedly for long-lived servers.
|
|
64
|
+
// Use a custom TaskStore with eviction for production HTTP deployments.
|
|
62
65
|
serverConfig.taskStore = new InMemoryTaskStore();
|
|
63
66
|
serverConfig.taskMessageQueue = new InMemoryTaskMessageQueue();
|
|
64
67
|
}
|
|
@@ -85,6 +88,7 @@ export async function createServer(options = {}) {
|
|
|
85
88
|
registerInstructionResource(server, serverInstructions, localIcon);
|
|
86
89
|
registerToolCatalogResource(server, localIcon);
|
|
87
90
|
registerWorkflowGuideResource(server, localIcon);
|
|
91
|
+
registerToolInfoResource(server, localIcon);
|
|
88
92
|
registerGetHelpPrompt(server, serverInstructions, localIcon);
|
|
89
93
|
registerResultResources(server, resourceStore, localIcon);
|
|
90
94
|
registerMetricsResource(server, localIcon);
|
|
@@ -198,11 +202,40 @@ function isAllowedOrigin(origin) {
|
|
|
198
202
|
return true; // Non-browser clients omit Origin.
|
|
199
203
|
return LOCALHOST_ORIGIN_RE.test(origin);
|
|
200
204
|
}
|
|
205
|
+
function getProtocolVersionHeader(req) {
|
|
206
|
+
const rawProtocolVersion = req.headers['mcp-protocol-version'];
|
|
207
|
+
if (typeof rawProtocolVersion === 'string') {
|
|
208
|
+
return rawProtocolVersion;
|
|
209
|
+
}
|
|
210
|
+
if (Array.isArray(rawProtocolVersion)) {
|
|
211
|
+
return rawProtocolVersion.find((value) => value === REQUIRED_MCP_PROTOCOL_VERSION);
|
|
212
|
+
}
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
function ensureProtocolVersionHeader(req, res) {
|
|
216
|
+
const protocolVersion = getProtocolVersionHeader(req);
|
|
217
|
+
if (protocolVersion === REQUIRED_MCP_PROTOCOL_VERSION) {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
sendJsonRpcError(res, 400, -32000, 'Bad Request: MCP-Protocol-Version header missing or unsupported');
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
function discardRequestBody(req) {
|
|
224
|
+
req.on('error', () => {
|
|
225
|
+
// Best effort drain to avoid corrupting keep-alive pipelines.
|
|
226
|
+
});
|
|
227
|
+
req.resume();
|
|
228
|
+
}
|
|
201
229
|
export async function startHttpServer(port, options) {
|
|
202
230
|
const sessions = new Map();
|
|
203
231
|
async function handleMcpRequest(req, res) {
|
|
204
232
|
const { method } = req;
|
|
205
|
-
const
|
|
233
|
+
const MAX_SESSION_ID_LENGTH = 256;
|
|
234
|
+
const rawSessionId = req.headers['mcp-session-id'];
|
|
235
|
+
const sessionId = typeof rawSessionId === 'string' &&
|
|
236
|
+
rawSessionId.length <= MAX_SESSION_ID_LENGTH
|
|
237
|
+
? rawSessionId
|
|
238
|
+
: undefined;
|
|
206
239
|
const { origin } = req.headers;
|
|
207
240
|
if (!isAllowedOrigin(origin)) {
|
|
208
241
|
sendJsonRpcError(res, 403, -32000, 'Forbidden: disallowed origin');
|
|
@@ -235,33 +268,59 @@ export async function startHttpServer(port, options) {
|
|
|
235
268
|
}
|
|
236
269
|
try {
|
|
237
270
|
if (method === 'POST') {
|
|
238
|
-
|
|
239
|
-
|
|
271
|
+
if (sessionId) {
|
|
272
|
+
if (!sessions.has(sessionId)) {
|
|
273
|
+
sendJsonRpcError(res, 404, -32000, 'Session not found');
|
|
274
|
+
discardRequestBody(req);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (!ensureProtocolVersionHeader(req, res)) {
|
|
278
|
+
discardRequestBody(req);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const body = await readRequestBody(req);
|
|
240
282
|
const session = sessions.get(sessionId);
|
|
241
283
|
if (session) {
|
|
242
284
|
await session.transport.handleRequest(req, res, body);
|
|
243
285
|
}
|
|
286
|
+
else {
|
|
287
|
+
sendJsonRpcError(res, 404, -32000, 'Session not found');
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
244
290
|
}
|
|
245
|
-
|
|
291
|
+
const body = await readRequestBody(req);
|
|
292
|
+
if (isInitializeRequest(body)) {
|
|
293
|
+
const maxSessions = parseInt(process.env['FILESYSTEM_MCP_MAX_HTTP_SESSIONS'] ?? '', 10) || 100;
|
|
294
|
+
if (sessions.size >= maxSessions) {
|
|
295
|
+
sendJsonRpcError(res, 503, -32000, 'Too many sessions');
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
246
298
|
const { transport } = await createHttpSession(options, sessions);
|
|
247
299
|
await transport.handleRequest(req, res, body);
|
|
300
|
+
return;
|
|
248
301
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
sendJsonRpcError(res, 400, -32000, 'Bad Request: No valid session ID provided');
|
|
254
|
-
}
|
|
302
|
+
sendJsonRpcError(res, 400, -32000, 'Bad Request: No valid session ID provided');
|
|
303
|
+
discardRequestBody(req);
|
|
255
304
|
}
|
|
256
305
|
else if (method === 'GET' || method === 'DELETE') {
|
|
257
|
-
if (!sessionId
|
|
258
|
-
sendJsonRpcError(res, 400, -32000, 'Bad Request:
|
|
306
|
+
if (!sessionId) {
|
|
307
|
+
sendJsonRpcError(res, 400, -32000, 'Bad Request: Missing session ID');
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (!sessions.has(sessionId)) {
|
|
311
|
+
sendJsonRpcError(res, 404, -32000, 'Session not found');
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (!ensureProtocolVersionHeader(req, res)) {
|
|
259
315
|
return;
|
|
260
316
|
}
|
|
261
317
|
const session = sessions.get(sessionId);
|
|
262
318
|
if (session) {
|
|
263
319
|
await session.transport.handleRequest(req, res);
|
|
264
320
|
}
|
|
321
|
+
else {
|
|
322
|
+
sendJsonRpcError(res, 404, -32000, 'Session not found');
|
|
323
|
+
}
|
|
265
324
|
}
|
|
266
325
|
else {
|
|
267
326
|
res.writeHead(405, { Allow: 'GET, POST, DELETE' });
|
|
@@ -27,6 +27,11 @@ export function buildServerCapabilities(options = {}) {
|
|
|
27
27
|
completions: {},
|
|
28
28
|
};
|
|
29
29
|
if (options.enableTaskToolRequests) {
|
|
30
|
+
// NOTE: enabling task tool requests requires the caller to configure
|
|
31
|
+
// an InMemoryTaskStore and InMemoryTaskMessageQueue on the McpServer.
|
|
32
|
+
// InMemoryTaskStore accumulates completed task records with no TTL eviction —
|
|
33
|
+
// suitable for short-lived stdio sessions. Long-running HTTP servers should
|
|
34
|
+
// replace it with a TTL-evicting store to avoid unbounded memory growth.
|
|
30
35
|
capabilities.tasks = {
|
|
31
36
|
list: {},
|
|
32
37
|
cancel: {},
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import
|
|
2
|
+
import type { z } from 'zod';
|
|
3
|
+
import { CreateDirectoryInputSchema, CreateDirectoryOutputSchema } from '../schemas.js';
|
|
4
|
+
import { type ToolContract, type ToolRegistrationOptions, type ToolResponse } from './shared.js';
|
|
3
5
|
export declare const CREATE_DIRECTORY_TOOL: ToolContract;
|
|
6
|
+
export declare function handleCreateDirectory(args: z.infer<typeof CreateDirectoryInputSchema>, signal?: AbortSignal): Promise<ToolResponse<z.infer<typeof CreateDirectoryOutputSchema>>>;
|
|
4
7
|
export declare function registerCreateDirectoryTool(server: McpServer, options?: ToolRegistrationOptions): void;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
|
-
import
|
|
3
|
-
import { ErrorCode } from '../lib/errors.js';
|
|
2
|
+
import { ErrorCode, McpError } from '../lib/errors.js';
|
|
4
3
|
import { withAbort } from '../lib/fs-helpers.js';
|
|
5
4
|
import { validatePathForWrite } from '../lib/path-validation.js';
|
|
6
5
|
import { CreateDirectoryInputSchema, CreateDirectoryOutputSchema, } from '../schemas.js';
|
|
@@ -15,12 +14,20 @@ export const CREATE_DIRECTORY_TOOL = {
|
|
|
15
14
|
annotations: IDEMPOTENT_WRITE_TOOL_ANNOTATIONS,
|
|
16
15
|
nuances: ['Succeeds silently if the directory already exists (idempotent).'],
|
|
17
16
|
};
|
|
18
|
-
async function handleCreateDirectory(args, signal) {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
export async function handleCreateDirectory(args, signal) {
|
|
18
|
+
const allPaths = [];
|
|
19
|
+
if (args.path)
|
|
20
|
+
allPaths.push(args.path);
|
|
21
|
+
if (args.paths)
|
|
22
|
+
allPaths.push(...args.paths);
|
|
23
|
+
if (allPaths.length === 0) {
|
|
24
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, 'No paths provided to create.');
|
|
25
|
+
}
|
|
26
|
+
const validPaths = await Promise.all(allPaths.map((p) => validatePathForWrite(p, signal)));
|
|
27
|
+
await Promise.all(validPaths.map((p) => withAbort(fs.mkdir(p, { recursive: true }), signal)));
|
|
28
|
+
return buildToolResponse(`Successfully created ${validPaths.length} director${validPaths.length === 1 ? 'y' : 'ies'}`, {
|
|
22
29
|
ok: true,
|
|
23
|
-
|
|
30
|
+
paths: validPaths,
|
|
24
31
|
});
|
|
25
32
|
}
|
|
26
33
|
export function registerCreateDirectoryTool(server, options = {}) {
|
|
@@ -28,21 +35,21 @@ export function registerCreateDirectoryTool(server, options = {}) {
|
|
|
28
35
|
toolName: 'mkdir',
|
|
29
36
|
extra,
|
|
30
37
|
timedSignal: {},
|
|
31
|
-
context: { path: args.path },
|
|
38
|
+
context: { path: args.path ?? args.paths?.[0] },
|
|
32
39
|
run: (signal) => handleCreateDirectory(args, signal),
|
|
33
|
-
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
|
|
40
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path ?? args.paths?.[0]),
|
|
34
41
|
});
|
|
35
42
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
36
43
|
guard: options.isInitialized,
|
|
37
44
|
progressMessage: (args) => {
|
|
38
|
-
const
|
|
39
|
-
return `🛠 mkdir: ${
|
|
45
|
+
const count = (args.path ? 1 : 0) + (args.paths?.length ?? 0);
|
|
46
|
+
return `🛠 mkdir: ${count} director${count === 1 ? 'y' : 'ies'}`;
|
|
40
47
|
},
|
|
41
48
|
completionMessage: (args, result) => {
|
|
42
|
-
const
|
|
49
|
+
const count = (args.path ? 1 : 0) + (args.paths?.length ?? 0);
|
|
43
50
|
if (result.isError)
|
|
44
|
-
return `🛠 mkdir: ${
|
|
45
|
-
return `🛠 mkdir: ${
|
|
51
|
+
return `🛠 mkdir: ${count} • failed`;
|
|
52
|
+
return `🛠 mkdir: ${count} • created`;
|
|
46
53
|
},
|
|
47
54
|
});
|
|
48
55
|
const validatedHandler = withValidatedArgs(CreateDirectoryInputSchema, wrappedHandler);
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import
|
|
2
|
+
import type { z } from 'zod';
|
|
3
|
+
import { EditFileInputSchema, EditFileOutputSchema } from '../schemas.js';
|
|
4
|
+
import { type ToolContract, type ToolRegistrationOptions, type ToolResponse } from './shared.js';
|
|
3
5
|
export declare const EDIT_FILE_TOOL: ToolContract;
|
|
6
|
+
export declare function handleEditFile(args: z.infer<typeof EditFileInputSchema>, signal?: AbortSignal): Promise<ToolResponse<z.infer<typeof EditFileOutputSchema>>>;
|
|
4
7
|
export declare function registerEditFileTool(server: McpServer, options?: ToolRegistrationOptions): void;
|
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';
|
|
@@ -23,28 +24,57 @@ export const EDIT_FILE_TOOL = {
|
|
|
23
24
|
'`oldText` must match exactly; unmatched items are reported in `unmatchedEdits`.',
|
|
24
25
|
],
|
|
25
26
|
};
|
|
26
|
-
function
|
|
27
|
+
function escapeRegExp(string) {
|
|
28
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
29
|
+
}
|
|
30
|
+
function applyEdits(content, edits, ignoreWhitespace) {
|
|
27
31
|
let newContent = content;
|
|
28
32
|
let appliedEdits = 0;
|
|
29
33
|
const unmatchedEdits = [];
|
|
30
34
|
let minLine;
|
|
31
35
|
let maxLine;
|
|
32
36
|
for (const edit of edits) {
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
if (ignoreWhitespace) {
|
|
38
|
+
const pattern = escapeRegExp(edit.oldText).replace(/\s+/g, '\\s+');
|
|
39
|
+
const regex = new RE2(pattern);
|
|
40
|
+
const match = regex.exec(newContent);
|
|
41
|
+
if (!match) {
|
|
42
|
+
unmatchedEdits.push(edit.oldText);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const { index } = match;
|
|
46
|
+
const matchLength = match[0].length;
|
|
47
|
+
const linesBefore = newContent.slice(0, index).split('\n').length;
|
|
48
|
+
const newTextLines = edit.newText.split('\n').length;
|
|
49
|
+
const startLine = linesBefore;
|
|
50
|
+
const endLine = linesBefore + newTextLines - 1;
|
|
51
|
+
if (minLine === undefined || startLine < minLine)
|
|
52
|
+
minLine = startLine;
|
|
53
|
+
if (maxLine === undefined || endLine > maxLine)
|
|
54
|
+
maxLine = endLine;
|
|
55
|
+
newContent =
|
|
56
|
+
newContent.slice(0, index) +
|
|
57
|
+
edit.newText +
|
|
58
|
+
newContent.slice(index + matchLength);
|
|
59
|
+
appliedEdits += 1;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
if (!newContent.includes(edit.oldText)) {
|
|
63
|
+
unmatchedEdits.push(edit.oldText);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const index = newContent.indexOf(edit.oldText);
|
|
67
|
+
const linesBefore = newContent.slice(0, index).split('\n').length;
|
|
68
|
+
const newTextLines = edit.newText.split('\n').length;
|
|
69
|
+
const startLine = linesBefore;
|
|
70
|
+
const endLine = linesBefore + newTextLines - 1;
|
|
71
|
+
if (minLine === undefined || startLine < minLine)
|
|
72
|
+
minLine = startLine;
|
|
73
|
+
if (maxLine === undefined || endLine > maxLine)
|
|
74
|
+
maxLine = endLine;
|
|
75
|
+
newContent = newContent.replace(edit.oldText, () => edit.newText);
|
|
76
|
+
appliedEdits += 1;
|
|
36
77
|
}
|
|
37
|
-
const index = newContent.indexOf(edit.oldText);
|
|
38
|
-
const linesBefore = newContent.slice(0, index).split('\n').length;
|
|
39
|
-
const newTextLines = edit.newText.split('\n').length;
|
|
40
|
-
const startLine = linesBefore;
|
|
41
|
-
const endLine = linesBefore + newTextLines - 1;
|
|
42
|
-
if (minLine === undefined || startLine < minLine)
|
|
43
|
-
minLine = startLine;
|
|
44
|
-
if (maxLine === undefined || endLine > maxLine)
|
|
45
|
-
maxLine = endLine;
|
|
46
|
-
newContent = newContent.replace(edit.oldText, () => edit.newText);
|
|
47
|
-
appliedEdits += 1;
|
|
48
78
|
}
|
|
49
79
|
const result = {
|
|
50
80
|
content: newContent,
|
|
@@ -56,10 +86,10 @@ function applyEdits(content, edits) {
|
|
|
56
86
|
}
|
|
57
87
|
return result;
|
|
58
88
|
}
|
|
59
|
-
async function handleEditFile(args, signal) {
|
|
89
|
+
export async function handleEditFile(args, signal) {
|
|
60
90
|
const validPath = await validateExistingPath(args.path, signal);
|
|
61
91
|
const content = await fs.readFile(validPath, { encoding: 'utf-8', signal });
|
|
62
|
-
const { content: newContent, appliedEdits, unmatchedEdits, lineRange, } = applyEdits(content, args.edits);
|
|
92
|
+
const { content: newContent, appliedEdits, unmatchedEdits, lineRange, } = applyEdits(content, args.edits, args.ignoreWhitespace);
|
|
63
93
|
const structured = {
|
|
64
94
|
ok: true,
|
|
65
95
|
path: validPath,
|
|
@@ -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({
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import
|
|
2
|
+
import type { z } from 'zod';
|
|
3
|
+
import { MoveFileInputSchema, MoveFileOutputSchema } from '../schemas.js';
|
|
4
|
+
import { type ToolContract, type ToolRegistrationOptions, type ToolResponse } from './shared.js';
|
|
3
5
|
export declare const MOVE_FILE_TOOL: ToolContract;
|
|
6
|
+
export declare function handleMoveFile(args: z.infer<typeof MoveFileInputSchema>, signal?: AbortSignal): Promise<ToolResponse<z.infer<typeof MoveFileOutputSchema>>>;
|
|
4
7
|
export declare function registerMoveFileTool(server: McpServer, options?: ToolRegistrationOptions): void;
|
package/dist/tools/move-file.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import { ErrorCode, isNodeError } from '../lib/errors.js';
|
|
3
|
+
import { ErrorCode, formatUnknownErrorMessage, isNodeError, McpError, } from '../lib/errors.js';
|
|
4
4
|
import { withAbort } from '../lib/fs-helpers.js';
|
|
5
5
|
import { validateExistingPath, validatePathForWrite, } from '../lib/path-validation.js';
|
|
6
6
|
import { MoveFileInputSchema, MoveFileOutputSchema } from '../schemas.js';
|
|
@@ -18,28 +18,95 @@ export const MOVE_FILE_TOOL = {
|
|
|
18
18
|
'On POSIX, an existing destination is silently overwritten; on Windows, rename fails with EEXIST if destination exists.',
|
|
19
19
|
],
|
|
20
20
|
};
|
|
21
|
-
async function handleMoveFile(args, signal) {
|
|
22
|
-
const
|
|
21
|
+
export async function handleMoveFile(args, signal) {
|
|
22
|
+
const sources = args.sources ?? (args.source ? [args.source] : []);
|
|
23
|
+
if (sources.length === 0) {
|
|
24
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, 'No sources provided.');
|
|
25
|
+
}
|
|
23
26
|
const validDest = await validatePathForWrite(args.destination, signal);
|
|
24
|
-
//
|
|
25
|
-
|
|
27
|
+
// Check if destination exists and is a directory
|
|
28
|
+
let destIsDirectory = false;
|
|
26
29
|
try {
|
|
27
|
-
await
|
|
30
|
+
const stats = await fs.stat(validDest);
|
|
31
|
+
destIsDirectory = stats.isDirectory();
|
|
28
32
|
}
|
|
29
33
|
catch (error) {
|
|
30
|
-
if (isNodeError(error) && error.code
|
|
31
|
-
// Cross-device link, fallback to copy + delete
|
|
32
|
-
await withAbort(fs.cp(validSource, validDest, { recursive: true }), signal);
|
|
33
|
-
await withAbort(fs.rm(validSource, { recursive: true, force: true }), signal);
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
34
|
+
if (isNodeError(error) && error.code !== 'ENOENT') {
|
|
36
35
|
throw error;
|
|
37
36
|
}
|
|
38
37
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
if (sources.length > 1 && !destIsDirectory) {
|
|
39
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Destination must be an existing directory when moving multiple files.');
|
|
40
|
+
}
|
|
41
|
+
// Ensure destination parent directory exists if it's not an existing directory
|
|
42
|
+
if (!destIsDirectory) {
|
|
43
|
+
await withAbort(fs.mkdir(path.dirname(validDest), { recursive: true }), signal);
|
|
44
|
+
}
|
|
45
|
+
const movedSources = [];
|
|
46
|
+
const failed = [];
|
|
47
|
+
for (const src of sources) {
|
|
48
|
+
let validSource;
|
|
49
|
+
try {
|
|
50
|
+
validSource = await validateExistingPath(src, signal);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
failed.push({
|
|
54
|
+
source: src,
|
|
55
|
+
error: formatUnknownErrorMessage(error),
|
|
56
|
+
});
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const targetPath = destIsDirectory
|
|
60
|
+
? path.join(validDest, path.basename(validSource))
|
|
61
|
+
: validDest;
|
|
62
|
+
// Prevent moving a file onto itself
|
|
63
|
+
if (path.resolve(validSource) === path.resolve(targetPath)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
// Prevent moving a directory into its own subdirectory
|
|
67
|
+
// Fixes "Missing validation for moving directory into its own subdirectory" finding
|
|
68
|
+
if (path.resolve(targetPath).startsWith(path.resolve(validSource) + path.sep)) {
|
|
69
|
+
failed.push({
|
|
70
|
+
source: src,
|
|
71
|
+
error: `Cannot move directory '${src}' into its own subdirectory '${targetPath}'`,
|
|
72
|
+
});
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
await withAbort(fs.rename(validSource, targetPath), signal);
|
|
77
|
+
movedSources.push(validSource);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (isNodeError(error) && error.code === 'EXDEV') {
|
|
81
|
+
// Cross-device link, fallback to copy + delete
|
|
82
|
+
try {
|
|
83
|
+
await withAbort(fs.cp(validSource, targetPath, { recursive: true }), signal);
|
|
84
|
+
await withAbort(fs.rm(validSource, { recursive: true, force: true }), signal);
|
|
85
|
+
movedSources.push(validSource);
|
|
86
|
+
}
|
|
87
|
+
catch (copyError) {
|
|
88
|
+
failed.push({
|
|
89
|
+
source: src,
|
|
90
|
+
error: formatUnknownErrorMessage(copyError),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
failed.push({
|
|
96
|
+
source: src,
|
|
97
|
+
error: formatUnknownErrorMessage(error),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const message = failed.length > 0
|
|
103
|
+
? `Moved ${movedSources.length} item${movedSources.length === 1 ? '' : 's'}; failed to move ${failed.length} item${failed.length === 1 ? '' : 's'}`
|
|
104
|
+
: `Successfully moved ${movedSources.length} item${movedSources.length === 1 ? '' : 's'} to ${args.destination}`;
|
|
105
|
+
return buildToolResponse(message, {
|
|
106
|
+
ok: failed.length === 0,
|
|
107
|
+
sources: movedSources,
|
|
42
108
|
destination: validDest,
|
|
109
|
+
...(failed.length > 0 ? { failed } : {}),
|
|
43
110
|
});
|
|
44
111
|
}
|
|
45
112
|
export function registerMoveFileTool(server, options = {}) {
|
|
@@ -47,19 +114,23 @@ export function registerMoveFileTool(server, options = {}) {
|
|
|
47
114
|
toolName: 'mv',
|
|
48
115
|
extra,
|
|
49
116
|
timedSignal: {},
|
|
50
|
-
context: { path: args.source },
|
|
117
|
+
context: { path: args.source ?? args.sources?.[0] },
|
|
51
118
|
run: (signal) => handleMoveFile(args, signal),
|
|
52
|
-
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.source),
|
|
119
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.source ?? args.sources?.[0]),
|
|
53
120
|
});
|
|
54
121
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
55
122
|
guard: options.isInitialized,
|
|
56
|
-
progressMessage: (args) =>
|
|
123
|
+
progressMessage: (args) => {
|
|
124
|
+
const count = (args.source ? 1 : 0) + (args.sources?.length ?? 0);
|
|
125
|
+
const dest = path.basename(args.destination);
|
|
126
|
+
return `🛠 mv: ${count} item${count === 1 ? '' : 's'} → ${dest}`;
|
|
127
|
+
},
|
|
57
128
|
completionMessage: (args, result) => {
|
|
58
|
-
const
|
|
59
|
-
const
|
|
129
|
+
const count = (args.source ? 1 : 0) + (args.sources?.length ?? 0);
|
|
130
|
+
const dest = path.basename(args.destination);
|
|
60
131
|
if (result.isError)
|
|
61
|
-
return `🛠 mv: ${
|
|
62
|
-
return `🛠 mv: ${
|
|
132
|
+
return `🛠 mv: ${count} → ${dest} • failed`;
|
|
133
|
+
return `🛠 mv: ${count} → ${dest} • moved`;
|
|
63
134
|
},
|
|
64
135
|
});
|
|
65
136
|
const validatedHandler = withValidatedArgs(MoveFileInputSchema, wrappedHandler);
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import
|
|
2
|
+
import type { z } from 'zod';
|
|
3
|
+
import { SearchAndReplaceInputSchema, SearchAndReplaceOutputSchema } from '../schemas.js';
|
|
4
|
+
import { type ToolContract, type ToolRegistrationOptions, type ToolResponse } from './shared.js';
|
|
3
5
|
export declare const SEARCH_AND_REPLACE_TOOL: ToolContract;
|
|
6
|
+
export declare function handleSearchAndReplace(args: z.infer<typeof SearchAndReplaceInputSchema>, signal?: AbortSignal, onProgress?: (progress: {
|
|
7
|
+
total?: number;
|
|
8
|
+
current: number;
|
|
9
|
+
}) => void): Promise<ToolResponse<z.infer<typeof SearchAndReplaceOutputSchema>>>;
|
|
4
10
|
export declare function registerSearchAndReplaceTool(server: McpServer, options?: ToolRegistrationOptions): void;
|
|
@@ -122,7 +122,8 @@ async function processEntry(entryPath, args, regex, maxFileSize, signal, summary
|
|
|
122
122
|
else {
|
|
123
123
|
newContent = content.replaceAll(args.searchPattern, () => args.replacement);
|
|
124
124
|
}
|
|
125
|
-
if (args.dryRun
|
|
125
|
+
if ((args.dryRun || args.returnDiff) &&
|
|
126
|
+
summary.diff.length < MAX_DIFF_SIZE) {
|
|
126
127
|
const patch = createTwoFilesPatch(path.basename(validPath), path.basename(validPath), content, newContent, 'Original', 'Modified');
|
|
127
128
|
// Only append if it won't exceed the limit too much
|
|
128
129
|
if (summary.diff.length + patch.length <= MAX_DIFF_SIZE + 1024) {
|
|
@@ -201,7 +202,7 @@ function reportReplaceProgress(onProgress, current, force = false) {
|
|
|
201
202
|
return;
|
|
202
203
|
onProgress({ current });
|
|
203
204
|
}
|
|
204
|
-
async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
|
|
205
|
+
export async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
|
|
205
206
|
const maxFileSize = MAX_TEXT_FILE_SIZE;
|
|
206
207
|
const root = await resolveSearchRoot(args.path, signal);
|
|
207
208
|
const regex = createReplacementRegex(args);
|
|
@@ -240,7 +241,9 @@ async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
|
|
|
240
241
|
? { changedFiles: summary.changedFiles }
|
|
241
242
|
: {}),
|
|
242
243
|
...(summary.changedFilesTruncated ? { changedFilesTruncated: true } : {}),
|
|
243
|
-
...(args.dryRun
|
|
244
|
+
...((args.dryRun || args.returnDiff) && summary.diff
|
|
245
|
+
? { diff: summary.diff }
|
|
246
|
+
: {}),
|
|
244
247
|
dryRun: args.dryRun,
|
|
245
248
|
});
|
|
246
249
|
}
|
|
@@ -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,11 +41,11 @@ 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;
|
|
51
50
|
}
|
|
52
51
|
export type ToolResult<T> = ToolResponse<T> | ToolErrorResponse;
|
package/dist/tools/shared.js
CHANGED
|
@@ -89,12 +89,6 @@ export function buildResourceLink(params) {
|
|
|
89
89
|
...(params.mimeType ? { mimeType: params.mimeType } : {}),
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
|
-
function buildContentBlock(text, structuredContent, extraContent = []) {
|
|
93
|
-
return {
|
|
94
|
-
content: [{ type: 'text', text }, ...extraContent],
|
|
95
|
-
structuredContent,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
92
|
function resolveDetailedError(error, defaultCode, path) {
|
|
99
93
|
const detailed = createDetailedError(error, path);
|
|
100
94
|
if (detailed.code === ErrorCode.E_UNKNOWN) {
|
|
@@ -104,7 +98,10 @@ function resolveDetailedError(error, defaultCode, path) {
|
|
|
104
98
|
return detailed;
|
|
105
99
|
}
|
|
106
100
|
export function buildToolResponse(text, structuredContent, extraContent = []) {
|
|
107
|
-
return
|
|
101
|
+
return {
|
|
102
|
+
content: [{ type: 'text', text }, ...extraContent],
|
|
103
|
+
structuredContent,
|
|
104
|
+
};
|
|
108
105
|
}
|
|
109
106
|
function parseToolArgs(schema, args) {
|
|
110
107
|
const candidate = args === undefined ? {} : args;
|
|
@@ -198,22 +195,8 @@ export async function executeToolWithDiagnostics(options) {
|
|
|
198
195
|
export function buildToolErrorResponse(error, defaultCode, path) {
|
|
199
196
|
const detailed = resolveDetailedError(error, defaultCode, path);
|
|
200
197
|
const text = formatDetailedError(detailed);
|
|
201
|
-
const errorContent = {
|
|
202
|
-
code: detailed.code,
|
|
203
|
-
message: detailed.message,
|
|
204
|
-
};
|
|
205
|
-
if (detailed.path !== undefined) {
|
|
206
|
-
errorContent.path = detailed.path;
|
|
207
|
-
}
|
|
208
|
-
if (detailed.suggestion !== undefined) {
|
|
209
|
-
errorContent.suggestion = detailed.suggestion;
|
|
210
|
-
}
|
|
211
|
-
const structuredContent = {
|
|
212
|
-
ok: false,
|
|
213
|
-
error: errorContent,
|
|
214
|
-
};
|
|
215
198
|
return {
|
|
216
|
-
|
|
199
|
+
content: [{ type: 'text', text }],
|
|
217
200
|
isError: true,
|
|
218
201
|
};
|
|
219
202
|
}
|
|
@@ -134,14 +134,17 @@ function normalizeCallToolResult(value) {
|
|
|
134
134
|
function getToolResultErrorCode(result) {
|
|
135
135
|
if (!isRecord(result) || result['isError'] !== true)
|
|
136
136
|
return undefined;
|
|
137
|
-
const
|
|
138
|
-
if (!
|
|
137
|
+
const { content } = result;
|
|
138
|
+
if (!Array.isArray(content) || content.length === 0)
|
|
139
139
|
return undefined;
|
|
140
|
-
const
|
|
141
|
-
if (!isRecord(
|
|
140
|
+
const first = content[0];
|
|
141
|
+
if (!isRecord(first) || first['type'] !== 'text')
|
|
142
142
|
return undefined;
|
|
143
|
-
const {
|
|
144
|
-
|
|
143
|
+
const { text } = first;
|
|
144
|
+
if (typeof text !== 'string')
|
|
145
|
+
return undefined;
|
|
146
|
+
const match = /^Error \[([A-Z0-9_]+)\]:/.exec(text);
|
|
147
|
+
return match ? match[1] : undefined;
|
|
145
148
|
}
|
|
146
149
|
function isCancelledToolResult(result) {
|
|
147
150
|
return getToolResultErrorCode(result) === ErrorCode.E_CANCELLED;
|
|
@@ -222,7 +225,7 @@ function getTaskId(extra) {
|
|
|
222
225
|
return extra.taskId;
|
|
223
226
|
}
|
|
224
227
|
function isErrorResult(result) {
|
|
225
|
-
return 'isError' in result && result.isError
|
|
228
|
+
return 'isError' in result && result.isError;
|
|
226
229
|
}
|
|
227
230
|
// 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
231
|
function withoutStructuredContent(result) {
|