@j0hanz/filesystem-mcp 1.10.0 → 1.11.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.
@@ -3,6 +3,7 @@ import * as path from 'node:path';
3
3
  import { CompleteRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
4
  import { getAllowedDirectories, isPathWithinDirectories, normalizePath, toPosixPath, } from './lib/paths.js';
5
5
  import { isRecord } from './lib/utils.js';
6
+ import { getSortedToolContracts } from './resources/tool-info.js';
6
7
  const MAX_COMPLETION_ITEMS = 100;
7
8
  const COMPLETION_RATE_LIMIT_MS = 100;
8
9
  const MAX_COMPLETION_CACHE_KEYS = 128;
@@ -29,6 +30,9 @@ function extractTopicCompletions(instructions) {
29
30
  }
30
31
  return headers;
31
32
  }
33
+ function extractToolNameCompletions() {
34
+ return getSortedToolContracts().map((contract) => contract.name);
35
+ }
32
36
  const PATH_ARGUMENTS = new Set([
33
37
  'path',
34
38
  'source',
@@ -415,6 +419,7 @@ export async function getPathCompletions(currentValue, options = {}) {
415
419
  }
416
420
  export function registerCompletions(server, instructions = '') {
417
421
  const topicValues = extractTopicCompletions(instructions);
422
+ const toolNameValues = extractToolNameCompletions();
418
423
  server.server.setRequestHandler(CompleteRequestSchema, async (request) => {
419
424
  const { params } = request;
420
425
  const { argument, ref } = params;
@@ -427,6 +432,26 @@ export function registerCompletions(server, instructions = '') {
427
432
  : topicValues;
428
433
  return buildCompletionResponse(buildCompletionResult(filtered));
429
434
  }
435
+ if (isRecord(ref) &&
436
+ ref['type'] === 'ref/prompt' &&
437
+ ref['name'] === 'get-tool-help' &&
438
+ argName === 'name') {
439
+ const currentValue = argument.value.toLowerCase();
440
+ const filtered = currentValue
441
+ ? toolNameValues.filter((value) => value.startsWith(currentValue))
442
+ : toolNameValues;
443
+ return buildCompletionResponse(buildCompletionResult(filtered));
444
+ }
445
+ if (isRecord(ref) &&
446
+ ref['type'] === 'ref/resource' &&
447
+ ref['uri'] === 'internal://tool-info/{name}' &&
448
+ argName === 'name') {
449
+ const currentValue = argument.value.toLowerCase();
450
+ const filtered = currentValue
451
+ ? toolNameValues.filter((value) => value.startsWith(currentValue))
452
+ : toolNameValues;
453
+ return buildCompletionResponse(buildCompletionResult(filtered));
454
+ }
430
455
  const isPathArg = isPathLikeArgumentName(argName) ||
431
456
  isPathArgumentFromReference(argName, ref);
432
457
  if (!isPathArg) {
package/dist/prompts.d.ts CHANGED
@@ -3,3 +3,4 @@ import { type IconInfo } from './tools/shared.js';
3
3
  export declare function registerGetHelpPrompt(server: McpServer, instructions: string, iconInfo?: IconInfo): void;
4
4
  export declare function registerCompareFilesPrompt(server: McpServer, iconInfo?: IconInfo): void;
5
5
  export declare function registerAnalyzePathPrompt(server: McpServer, iconInfo?: IconInfo): void;
6
+ export declare function registerGetToolHelpPrompt(server: McpServer, iconInfo?: IconInfo): void;
package/dist/prompts.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { z } from 'zod';
2
+ import { ErrorCode, McpError } from './lib/errors.js';
3
+ import { buildToolInfo, getSortedToolContracts, } from './resources/tool-info.js';
2
4
  import { withDefaultIcons } from './tools/shared.js';
3
5
  const HELP_PROMPT_NAME = 'get-help';
4
6
  const HELP_PROMPT_TITLE = 'Get Help';
@@ -9,6 +11,9 @@ const COMPARE_FILES_PROMPT_DESCRIPTION = 'Generate a workflow for comparing two
9
11
  const ANALYZE_PATH_PROMPT_NAME = 'analyze-path';
10
12
  const ANALYZE_PATH_PROMPT_TITLE = 'Analyze Path';
11
13
  const ANALYZE_PATH_PROMPT_DESCRIPTION = 'Generate a workflow for analyzing a file or directory using stat, read, and tree.';
14
+ const GET_TOOL_HELP_PROMPT_NAME = 'get-tool-help';
15
+ const GET_TOOL_HELP_PROMPT_TITLE = 'Get Tool Help';
16
+ const GET_TOOL_HELP_PROMPT_DESCRIPTION = 'Return a prompt with the authoritative contract for a specific filesystem-mcp tool.';
12
17
  function filterInstructionsByTopic(instructions, topic) {
13
18
  const normalized = topic.trim().toLowerCase();
14
19
  if (!normalized)
@@ -24,6 +29,12 @@ function filterInstructionsByTopic(instructions, topic) {
24
29
  .join(', ');
25
30
  return `Section '${topic}' not found. Available: ${available}\n\n${instructions}`;
26
31
  }
32
+ function findKnownToolName(rawName) {
33
+ const normalized = rawName.trim().toLowerCase();
34
+ if (!normalized)
35
+ return undefined;
36
+ return getSortedToolContracts().find((contract) => contract.name.toLowerCase() === normalized)?.name;
37
+ }
27
38
  export function registerGetHelpPrompt(server, instructions, iconInfo) {
28
39
  const baseConfig = withDefaultIcons({ title: HELP_PROMPT_TITLE, description: HELP_PROMPT_DESCRIPTION }, iconInfo);
29
40
  server.registerPrompt(HELP_PROMPT_NAME, {
@@ -97,3 +108,50 @@ export function registerAnalyzePathPrompt(server, iconInfo) {
97
108
  ],
98
109
  }));
99
110
  }
111
+ export function registerGetToolHelpPrompt(server, iconInfo) {
112
+ server.registerPrompt(GET_TOOL_HELP_PROMPT_NAME, {
113
+ ...withDefaultIcons({
114
+ title: GET_TOOL_HELP_PROMPT_TITLE,
115
+ description: GET_TOOL_HELP_PROMPT_DESCRIPTION,
116
+ }, iconInfo),
117
+ argsSchema: {
118
+ name: z
119
+ .string()
120
+ .min(1)
121
+ .describe('Tool name from tools/list or internal://tool-info/{name}.'),
122
+ },
123
+ }, ({ name }) => {
124
+ const toolName = findKnownToolName(name);
125
+ if (!toolName) {
126
+ throw new McpError(ErrorCode.E_INVALID_INPUT, `Unknown tool: ${name}`);
127
+ }
128
+ const toolInfo = buildToolInfo(toolName);
129
+ if (!toolInfo) {
130
+ throw new McpError(ErrorCode.E_INVALID_INPUT, `Unknown tool: ${toolName}`);
131
+ }
132
+ return {
133
+ description: GET_TOOL_HELP_PROMPT_DESCRIPTION,
134
+ messages: [
135
+ {
136
+ role: 'user',
137
+ content: {
138
+ type: 'text',
139
+ text: `Use the embedded contract for \`${toolName}\` as the authoritative reference. ` +
140
+ 'Summarize when to use it, its key constraints, and the safest next action.',
141
+ },
142
+ },
143
+ {
144
+ role: 'user',
145
+ content: {
146
+ type: 'resource',
147
+ resource: {
148
+ uri: `internal://tool-info/${toolName}`,
149
+ mimeType: 'text/markdown',
150
+ text: toolInfo,
151
+ },
152
+ },
153
+ },
154
+ ],
155
+ };
156
+ });
157
+ }
@@ -1,6 +1,9 @@
1
1
  import { buildToolCatalogDetailsOnly } from './tool-catalog.js';
2
- import { buildCoreContextPack, formatToolNameList, getSharedConstraints, getTaskCapableToolNames, getToolContracts, pickAvailableToolNames, } from './tool-info.js';
2
+ import { buildCoreContextPack, formatToolNameList, getSharedConstraints, getTaskCapableToolNames, getTaskToolNamesBySupport, getToolContracts, pickAvailableToolNames, } from './tool-info.js';
3
3
  import { buildWorkflowGuide } from './workflows.js';
4
+ function formatTaskModeLine(label, names) {
5
+ return `${label}: ${names.length > 0 ? formatToolNameList(names) : 'none'}.`;
6
+ }
4
7
  function buildToolsOverview() {
5
8
  const rows = [
6
9
  ['Navigate', pickAvailableToolNames(['roots', 'ls', 'tree', 'find'])],
@@ -29,6 +32,8 @@ function buildToolsOverview() {
29
32
  }
30
33
  function buildInstructionsHeader() {
31
34
  const taskCapable = formatToolNameList(getTaskCapableToolNames());
35
+ const optionalTaskTools = getTaskToolNamesBySupport('optional');
36
+ const requiredTaskTools = getTaskToolNamesBySupport('required');
32
37
  return `<role>
33
38
  Filesystem agent. Scope: allowed roots only. Discover paths before acting — never guess.
34
39
  </role>
@@ -49,9 +54,12 @@ ${buildToolsOverview()}
49
54
  </resources>
50
55
 
51
56
  <task_protocol>
52
- Task execution: Tools returning a task ID must be polled via \`tasks/get\`, then retrieved via \`tasks/result\`.
57
+ Task execution: Call task-capable tools inline by default; add \`task\` only when durable polling or deferred results are needed.
58
+ Task results: When a task is requested, poll via \`tasks/get\`, then retrieve the final payload via \`tasks/result\`.
53
59
  Progress: Pass \`_meta.progressToken\` in \`tools/call\` to receive \`notifications/progress\`.
54
- Task-capable: ${taskCapable}.
60
+ Task-capable: ${taskCapable || 'none'}.
61
+ ${formatTaskModeLine('Optional task mode', optionalTaskTools)}
62
+ ${formatTaskModeLine('Required task mode', requiredTaskTools)}
55
63
  </task_protocol>
56
64
  `;
57
65
  }
@@ -1,4 +1,4 @@
1
- import { buildCoreContextPack, pickAvailableToolNames } from './tool-info.js';
1
+ import { buildCoreContextPack, getTaskCapableToolNames, pickAvailableToolNames, } from './tool-info.js';
2
2
  function buildCrossToolDataFlow() {
3
3
  const flows = [];
4
4
  if (pickAvailableToolNames(['find', 'read']).length === 2) {
@@ -14,13 +14,59 @@ function buildCrossToolDataFlow() {
14
14
  return flows.join('\n');
15
15
  }
16
16
  function buildCatalogGuide() {
17
- return `<tool_selection_guide>
17
+ const taskCapable = getTaskCapableToolNames();
18
+ return (`<tool_selection_guide>
19
+ ## Primitive Routing
20
+
21
+ - ` +
22
+ '`tools`' +
23
+ `: model-controlled operations that inspect or mutate the allowed filesystem.
24
+ - ` +
25
+ '`resources`' +
26
+ `: application-driven context such as ` +
27
+ '`internal://instructions`' +
28
+ `, ` +
29
+ '`internal://tool-info/{name}`' +
30
+ `, and cached ` +
31
+ '`filesystem-mcp://result/{id}`' +
32
+ ` output.
33
+ - ` +
34
+ '`prompts`' +
35
+ `: user-controlled workflow templates for help, comparison, and guided inspection.
36
+ - ` +
37
+ '`completion`' +
38
+ `: argument suggestions for prompts and resource templates; not a discovery mechanism.
39
+
18
40
  ## Cross-Tool Data Flow
19
41
 
20
42
  \`\`\`
21
43
  ${buildCrossToolDataFlow()}
22
44
  \`\`\`
23
45
 
46
+ ## Result Contract
47
+
48
+ - Successful tools return ` +
49
+ '`content`' +
50
+ ` and usually ` +
51
+ '`structuredContent`' +
52
+ `.
53
+ - Tool/business failures return ` +
54
+ '`isError: true`' +
55
+ ` inside the tool result, not a JSON-RPC protocol error.
56
+ - When a tool returns ` +
57
+ '`resourceUri`' +
58
+ ` or a ` +
59
+ '`resource_link`' +
60
+ `, follow it with ` +
61
+ '`resources/read`' +
62
+ ` immediately.
63
+
64
+ ## Task Mode Routing
65
+
66
+ - Inline first for fast operations.
67
+ - Add task mode only when the caller needs durable polling, deferred retrieval, or cancellation after the initial response.
68
+ - Task-capable tools: ${taskCapable.length > 0 ? taskCapable.map((name) => `\`${name}\``).join(', ') : 'none'}.
69
+
24
70
  ## Search Strategy
25
71
 
26
72
  - \`find\`: glob file discovery.
@@ -47,7 +93,7 @@ ${buildCrossToolDataFlow()}
47
93
  - \`apply_patch\` accepts unified diffs - single-file or multi-file.
48
94
  - Multi-file: \`path\` is base directory; each file is best-effort with per-file \`results[]\`.
49
95
  </tool_selection_guide>
50
- `;
96
+ `);
51
97
  }
52
98
  export function buildToolCatalog() {
53
99
  return `${buildCoreContextPack()}\n\n${buildCatalogGuide()}`;
@@ -4,6 +4,7 @@ export declare function getSortedToolContracts(): ToolContract[];
4
4
  export declare function pickAvailableToolNames(names: readonly string[]): string[];
5
5
  export declare function formatToolNameList(names: readonly string[]): string;
6
6
  export declare function getTaskCapableToolNames(): string[];
7
+ export declare function getTaskToolNamesBySupport(taskSupport: Extract<ToolContract['taskSupport'], 'optional' | 'required'>): string[];
7
8
  export declare function buildCoreContextPack(): string;
8
9
  export declare function getSharedConstraints(): string[];
9
10
  export declare function buildToolInfo(name: string): string | undefined;
@@ -1,5 +1,16 @@
1
+ import { z } from 'zod';
1
2
  import { DEFAULT_SEARCH_CONTENT_RESULTS, MAX_SEARCH_RESULTS, MAX_TEXT_FILE_SIZE, } from '../lib/constants.js';
2
3
  import { ALL_TOOLS } from '../tools.js';
4
+ function getTaskSupportLabel(taskSupport) {
5
+ switch (taskSupport) {
6
+ case 'optional':
7
+ return '[Task: Optional]';
8
+ case 'required':
9
+ return '[Task: Required]';
10
+ default:
11
+ return undefined;
12
+ }
13
+ }
3
14
  function toEntry(contract) {
4
15
  const annotations = [];
5
16
  if (contract.annotations?.destructiveHint)
@@ -8,11 +19,12 @@ function toEntry(contract) {
8
19
  annotations.push('[Idempotent]');
9
20
  if (contract.annotations?.readOnlyHint)
10
21
  annotations.push('[Read-Only]');
11
- if (contract.taskSupport === 'optional' ||
12
- contract.taskSupport === 'required')
13
- annotations.push('[Task]');
22
+ const taskLabel = getTaskSupportLabel(contract.taskSupport);
23
+ if (taskLabel)
24
+ annotations.push(taskLabel);
14
25
  return {
15
26
  name: contract.name,
27
+ title: contract.title,
16
28
  description: contract.description,
17
29
  ...(annotations.length > 0 ? { annotations } : {}),
18
30
  ...(contract.nuances && contract.nuances.length > 0
@@ -43,6 +55,11 @@ export function getTaskCapableToolNames() {
43
55
  contract.taskSupport === 'required')
44
56
  .map((contract) => contract.name);
45
57
  }
58
+ export function getTaskToolNamesBySupport(taskSupport) {
59
+ return getSortedToolContracts()
60
+ .filter((contract) => contract.taskSupport === taskSupport)
61
+ .map((contract) => contract.name);
62
+ }
46
63
  export function buildCoreContextPack() {
47
64
  const rows = getSortedToolContracts().map((contract) => {
48
65
  const e = ENTRIES[contract.name];
@@ -61,25 +78,135 @@ export function getSharedConstraints() {
61
78
  'If response includes `resourceUri`, call `resources/read` immediately — cached results expire on restart.',
62
79
  ];
63
80
  }
81
+ function formatTaskSupportLabel(taskSupport) {
82
+ switch (taskSupport) {
83
+ case 'optional':
84
+ return 'optional';
85
+ case 'required':
86
+ return 'required';
87
+ default:
88
+ return 'forbidden';
89
+ }
90
+ }
91
+ function formatAnnotationValue(value) {
92
+ return value ? 'true' : 'false';
93
+ }
94
+ function toJsonSchemaObject(schema) {
95
+ return z.toJSONSchema(schema);
96
+ }
97
+ function summarizeSchemaType(schema) {
98
+ if (Array.isArray(schema.enum) && schema.enum.length > 0) {
99
+ return `enum(${schema.enum.map((value) => JSON.stringify(value)).join(', ')})`;
100
+ }
101
+ if (Array.isArray(schema.anyOf) && schema.anyOf.length > 0) {
102
+ return schema.anyOf.map(summarizeSchemaType).join(' | ');
103
+ }
104
+ if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) {
105
+ return schema.oneOf.map(summarizeSchemaType).join(' | ');
106
+ }
107
+ if (schema.type === 'array') {
108
+ const itemType = schema.items
109
+ ? summarizeSchemaType(schema.items)
110
+ : 'unknown';
111
+ return `array<${itemType}>`;
112
+ }
113
+ if (typeof schema.type === 'string' && schema.type.length > 0) {
114
+ return schema.type;
115
+ }
116
+ return 'unknown';
117
+ }
118
+ function buildSchemaFieldLines(label, schema) {
119
+ if (!schema) {
120
+ return [`<${label}>`, '- none', `</${label}>`];
121
+ }
122
+ const jsonSchema = toJsonSchemaObject(schema);
123
+ const properties = jsonSchema.properties ?? {};
124
+ const required = new Set(jsonSchema.required ?? []);
125
+ const fieldNames = Object.keys(properties);
126
+ if (fieldNames.length === 0) {
127
+ return [`<${label}>`, '- object with no fields', `</${label}>`];
128
+ }
129
+ return [
130
+ `<${label}>`,
131
+ ...fieldNames.map((fieldName) => {
132
+ const fieldSchema = properties[fieldName] ?? {};
133
+ const type = summarizeSchemaType(fieldSchema);
134
+ const requiredLabel = required.has(fieldName) ? 'required' : 'optional';
135
+ const description = typeof fieldSchema.description === 'string' &&
136
+ fieldSchema.description.length > 0
137
+ ? fieldSchema.description
138
+ : 'No description.';
139
+ return `- ${fieldName} (${type}, ${requiredLabel}): ${description}`;
140
+ }),
141
+ `</${label}>`,
142
+ ];
143
+ }
144
+ function buildProtocolNotes(contract) {
145
+ const notes = [
146
+ '- Protocol failures use JSON-RPC `error`; execution failures use tool result `isError: true`.',
147
+ ];
148
+ if (contract.outputSchema) {
149
+ notes.push('- Successful responses include `structuredContent` that must match the declared output schema.');
150
+ }
151
+ if (contract.taskSupport === 'optional') {
152
+ notes.push('- Supports inline execution by default and task mode when durable polling or deferred results are needed.');
153
+ }
154
+ if (contract.taskSupport === 'required') {
155
+ notes.push('- Must run in task mode; callers should poll `tasks/get` and fetch the payload via `tasks/result`.');
156
+ }
157
+ if (contract.taskSupport === 'forbidden') {
158
+ notes.push('- Runs inline only; task augmentation is not supported for this tool.');
159
+ }
160
+ return notes;
161
+ }
64
162
  export function buildToolInfo(name) {
163
+ const contract = CONTRACTS_BY_NAME.get(name);
65
164
  const entry = ENTRIES[name];
66
- if (!entry)
165
+ if (!entry || !contract)
67
166
  return undefined;
68
- const lines = [`## ${entry.name}`, '', entry.description];
167
+ const lines = [
168
+ `<tool_info name="${entry.name}">`,
169
+ `## ${entry.name}`,
170
+ '',
171
+ `Title: ${entry.title}`,
172
+ `Description: ${entry.description}`,
173
+ '',
174
+ '<execution>',
175
+ `- taskSupport: ${formatTaskSupportLabel(contract.taskSupport)}`,
176
+ '</execution>',
177
+ '',
178
+ '<annotations>',
179
+ `- readOnlyHint: ${formatAnnotationValue(contract.annotations?.readOnlyHint)}`,
180
+ `- idempotentHint: ${formatAnnotationValue(contract.annotations?.idempotentHint)}`,
181
+ `- destructiveHint: ${formatAnnotationValue(contract.annotations?.destructiveHint)}`,
182
+ `- openWorldHint: ${formatAnnotationValue(contract.annotations?.openWorldHint)}`,
183
+ '</annotations>',
184
+ '',
185
+ ...buildSchemaFieldLines('input_fields', contract.inputSchema),
186
+ '',
187
+ ...buildSchemaFieldLines('output_fields', contract.outputSchema),
188
+ '',
189
+ '<protocol_notes>',
190
+ ...buildProtocolNotes(contract),
191
+ '</protocol_notes>',
192
+ ];
69
193
  if (entry.annotations && entry.annotations.length > 0) {
70
- lines.push('', `**Hints:** ${entry.annotations.join(', ')}`);
194
+ lines.push('', `<quick_hints>${entry.annotations.join(' ')}</quick_hints>`);
71
195
  }
72
196
  if (entry.nuances && entry.nuances.length > 0) {
73
- lines.push('', '**Nuances:**');
197
+ lines.push('', '<nuances>');
74
198
  for (const nuance of entry.nuances) {
75
199
  lines.push(`- ${nuance}`);
76
200
  }
201
+ lines.push('</nuances>');
77
202
  }
78
203
  if (entry.gotchas && entry.gotchas.length > 0) {
79
- lines.push('', '**Gotchas:**');
204
+ lines.push('', '<gotchas>');
80
205
  for (const gotcha of entry.gotchas) {
81
206
  lines.push(`- ${gotcha}`);
82
207
  }
208
+ lines.push('</gotchas>');
83
209
  }
210
+ lines.push('</tool_info>');
84
211
  return lines.join('\n');
85
212
  }
package/dist/resources.js CHANGED
@@ -13,6 +13,8 @@ const TOOL_INFO_TEMPLATE = new ResourceTemplate('internal://tool-info/{name}', {
13
13
  resources: getToolContracts().map((contract) => ({
14
14
  uri: `internal://tool-info/${contract.name}`,
15
15
  name: contract.name,
16
+ title: contract.title,
17
+ description: contract.description,
16
18
  mimeType: 'text/markdown',
17
19
  })),
18
20
  }),
package/dist/schemas.d.ts CHANGED
@@ -63,8 +63,8 @@ export declare const SearchFilesInputSchema: z.ZodObject<{
63
63
  sortBy: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
64
64
  name: "name";
65
65
  size: "size";
66
- path: "path";
67
66
  modified: "modified";
67
+ path: "path";
68
68
  }>>>;
69
69
  maxDepth: z.ZodOptional<z.ZodInt>;
70
70
  cursor: z.ZodOptional<z.ZodString>;
@@ -556,6 +556,8 @@ export declare const EditFileOutputSchema: z.ZodObject<{
556
556
  ok: z.ZodBoolean;
557
557
  path: z.ZodOptional<z.ZodString>;
558
558
  appliedEdits: z.ZodOptional<z.ZodNumber>;
559
+ linesAdded: z.ZodOptional<z.ZodNumber>;
560
+ linesRemoved: z.ZodOptional<z.ZodNumber>;
559
561
  lineRange: z.ZodOptional<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>;
560
562
  unmatchedEdits: z.ZodOptional<z.ZodArray<z.ZodString>>;
561
563
  diff: z.ZodOptional<z.ZodString>;
package/dist/schemas.js CHANGED
@@ -488,6 +488,8 @@ export const EditFileOutputSchema = z.strictObject({
488
488
  ok: z.boolean(),
489
489
  path: z.string().optional(),
490
490
  appliedEdits: z.number().optional(),
491
+ linesAdded: z.number().optional().describe('Lines added'),
492
+ linesRemoved: z.number().optional().describe('Lines removed'),
491
493
  lineRange: z
492
494
  .tuple([z.number(), z.number()])
493
495
  .optional()
@@ -13,7 +13,7 @@ import { createInMemoryResourceStore } from '../lib/resource-store.js';
13
13
  import { isRecord } from '../lib/utils.js';
14
14
  import { registerCompletions } from '../completions.js';
15
15
  import { pkgInfo } from '../pkg-info.js';
16
- import { registerAnalyzePathPrompt, registerCompareFilesPrompt, registerGetHelpPrompt, } from '../prompts.js';
16
+ import { registerAnalyzePathPrompt, registerCompareFilesPrompt, registerGetHelpPrompt, registerGetToolHelpPrompt, } from '../prompts.js';
17
17
  import { registerInstructionResource, registerMetricsResource, registerResultResources, registerToolCatalogResource, registerToolInfoResource, registerWorkflowGuideResource, } from '../resources.js';
18
18
  import { buildServerInstructions } from '../resources/generated-instructions.js';
19
19
  import { registerAllTools } from '../tools.js';
@@ -174,6 +174,7 @@ export async function createServer(options = {}) {
174
174
  registerGetHelpPrompt(server, serverInstructions, localIcon);
175
175
  registerCompareFilesPrompt(server, localIcon);
176
176
  registerAnalyzePathPrompt(server, localIcon);
177
+ registerGetToolHelpPrompt(server, localIcon);
177
178
  registerResultResources(server, resourceStore, localIcon);
178
179
  registerMetricsResource(server, localIcon);
179
180
  registerCompletions(server, serverInstructions);
@@ -22,7 +22,7 @@ export const APPLY_PATCH_TOOL = {
22
22
  'Multi-file patches use `path` as base directory; per-file results in `results[]`.',
23
23
  ],
24
24
  gotchas: ['Patch must include valid hunk headers; use `dryRun=true` first.'],
25
- taskSupport: 'optional',
25
+ taskSupport: 'forbidden',
26
26
  };
27
27
  function assertPatchTargetSizeWithinLimit(filePath, size, maxFileSize) {
28
28
  if (size <= maxFileSize)
@@ -194,9 +194,12 @@ export function registerApplyPatchTool(server, options = {}) {
194
194
  const sc = result.structuredContent;
195
195
  if (!sc.ok)
196
196
  return `🛠 patch: ${name} • failed`;
197
- if (args.dryRun)
198
- return `🛠 patch: ${name} dry run OK`;
199
- return `🛠 patch: ${name} applied`;
197
+ const added = sc.linesAdded ?? 0;
198
+ const removed = sc.linesRemoved ?? 0;
199
+ const dry = args.dryRun ? 'dry run — ' : '';
200
+ if (added > 0 || removed > 0)
201
+ return `🛠 patch: ${name} • ${dry} +${added} -${removed}`;
202
+ return `🛠 patch: ${name} • ${dry}no changes`;
200
203
  },
201
204
  });
202
205
  const validatedHandler = withValidatedArgs(ApplyPatchInputSchema, wrappedHandler);
@@ -39,7 +39,7 @@ export interface ToolContract {
39
39
  */
40
40
  gotchas?: string[];
41
41
  /**
42
- * Task support level for the tool. Defaults to 'optional'.
42
+ * Task support level for the tool. Defaults to 'forbidden'.
43
43
  */
44
44
  taskSupport?: 'optional' | 'required' | 'forbidden';
45
45
  }
@@ -14,7 +14,7 @@ export const CREATE_DIRECTORY_TOOL = {
14
14
  outputSchema: CreateDirectoryOutputSchema,
15
15
  annotations: IDEMPOTENT_WRITE_TOOL_ANNOTATIONS,
16
16
  nuances: ['Succeeds silently if the directory already exists (idempotent).'],
17
- taskSupport: 'optional',
17
+ taskSupport: 'forbidden',
18
18
  };
19
19
  export async function handleCreateDirectory(args, signal) {
20
20
  const allPaths = [];
@@ -55,12 +55,12 @@ export function registerCreateDirectoryTool(server, options = {}) {
55
55
  const name = path.basename(args.path);
56
56
  if (result.isError)
57
57
  return `🛠 mkdir: ${name} • failed`;
58
- return `🛠 mkdir: ${name} • created`;
58
+ return `🛠 mkdir: ${name}`;
59
59
  }
60
60
  const count = (args.path ? 1 : 0) + (args.paths?.length ?? 0);
61
61
  if (result.isError)
62
62
  return `🛠 mkdir: ${count} directories • failed`;
63
- return `🛠 mkdir: ${count} directories • created`;
63
+ return `🛠 mkdir: ${count} directories`;
64
64
  },
65
65
  });
66
66
  const validatedHandler = withValidatedArgs(CreateDirectoryInputSchema, wrappedHandler);
@@ -17,7 +17,7 @@ export const DELETE_FILE_TOOL = {
17
17
  'No undo — deletion is permanent.',
18
18
  'Non-empty directories require `recursive=true`.',
19
19
  ],
20
- taskSupport: 'optional',
20
+ taskSupport: 'forbidden',
21
21
  };
22
22
  async function handleDeleteFile(args, signal) {
23
23
  const validPath = await validatePathForWrite(args.path, signal);
@@ -90,7 +90,7 @@ export function registerDeleteFileTool(server, options = {}) {
90
90
  const name = path.basename(args.path);
91
91
  if (result.isError)
92
92
  return `🛠 rm: ${name} • failed`;
93
- return `🛠 rm: ${name} • deleted`;
93
+ return `🛠 rm: ${name}`;
94
94
  },
95
95
  });
96
96
  const validatedHandler = withValidatedArgs(DeleteFileInputSchema, wrappedHandler);
@@ -18,7 +18,7 @@ export const DIFF_FILES_TOOL = {
18
18
  outputSchema: DiffFilesOutputSchema,
19
19
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
20
20
  gotchas: ['`isIdentical=true` means no hunks (`@@`) and empty diff.'],
21
- taskSupport: 'optional',
21
+ taskSupport: 'forbidden',
22
22
  };
23
23
  function computeDiffStats(patch) {
24
24
  let linesAdded = 0;
@@ -119,7 +119,11 @@ export function registerDiffFilesTool(server, options = {}) {
119
119
  return `🕮 diff: ${n1} ⟷ ${n2} • failed`;
120
120
  if (sc.isIdentical)
121
121
  return `🕮 diff: ${n1} ⟷ ${n2} • identical`;
122
- return `🕮 diff: ${n1} ${n2} • changed`;
122
+ const added = sc.linesAdded ?? 0;
123
+ const removed = sc.linesRemoved ?? 0;
124
+ if (added > 0 || removed > 0)
125
+ return `🕮 diff: ${n1} ⟷ ${n2} • +${added} -${removed}`;
126
+ return `🕮 diff: ${n1} ⟷ ${n2}`;
123
127
  },
124
128
  });
125
129
  const validatedHandler = withValidatedArgs(DiffFilesInputSchema, wrappedHandler);