Skip to content

Commit 85664e9

Browse files
authored
feat(workspace): support tool name remapping in workspace tools config (#13687)
## Summary - Adds a `name` property to `WorkspaceToolConfig` so workspace tools can be exposed under custom names to the LLM - Mastracode uses this to remap workspace tools back to its original names (`view`, `search_content`, `string_replace_lsp`, etc.) so they match the tool guidance prompts - Removes hardcoded tool-name cross-references from `edit-file` and `ast-edit` descriptions/output, since tools can be renamed or disabled - Extracts tool names into `MC_TOOLS` constants and `TOOL_NAME_OVERRIDES` mapping in `mastracode/src/tool-names.ts` for reuse across permissions, TUI, subagents, and tool guidance - Fixes pre-existing test failures in mastracode's `extra-tools.test.ts` that still expected workspace tools in `createDynamicTools()` output ## Test plan - [x] All 19 core workspace tool creation tests pass (including 6 new name remapping tests) - [x] All 14 mastracode extra-tools tests pass - [x] Core and mastracode packages build cleanly - [x] Verify mastracode works end-to-end with remapped tool names <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Tools can now be exposed under custom names while maintaining their original configuration identifiers. * **Documentation** * Updated tool descriptions to remove implementation-specific references for improved clarity. * **Refactor** * Standardized tool name references throughout the codebase for consistency and maintainability. * **Tests** * Added comprehensive test coverage for custom tool name remapping scenarios. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: CalebBarnes <CalebBarnes@users.noreply.github.com>
1 parent a1abe9e commit 85664e9

18 files changed

Lines changed: 311 additions & 113 deletions

File tree

.changeset/open-crews-lie.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@mastra/core': minor
3+
---
4+
5+
Added `name` property to `WorkspaceToolConfig` for remapping workspace tool names. Tools can now be exposed under custom names to the LLM while keeping the original constant as the config key.
6+
7+
```typescript
8+
const workspace = new Workspace({
9+
filesystem: new LocalFilesystem({ basePath: './project' }),
10+
tools: {
11+
mastra_workspace_read_file: { name: 'view' },
12+
mastra_workspace_grep: { name: 'search_content' },
13+
mastra_workspace_edit_file: { name: 'string_replace_lsp' },
14+
},
15+
});
16+
```
17+
18+
Also removed hardcoded tool-name cross-references from edit-file and ast-edit tool descriptions, since tools can be renamed or disabled.

.changeset/perky-glasses-make.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'mastracode': patch
3+
---
4+
5+
Workspace tool names are now remapped to canonical names (`view`, `search_content`, `string_replace_lsp`, etc.) so they match tool guidance prompts, permissions, and TUI rendering.

mastracode/src/agents/__tests__/extra-tools.test.ts

Lines changed: 42 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { describe, it, expect } from 'vitest';
44
import z from 'zod';
55

66
import { getToolCategory } from '../../permissions.js';
7+
import { MC_TOOLS } from '../../tool-names.js';
78
import { buildToolGuidance } from '../prompts/tool-guidance.js';
89
import { createDynamicTools } from '../tools.js';
910

@@ -43,26 +44,23 @@ describe('createDynamicTools – extraTools', () => {
4344
expect(tools).toHaveProperty('my_custom_tool');
4445
expect(tools.my_custom_tool).toBe(myCustomTool);
4546

46-
// Built-in tools should still be present
47-
expect(tools).toHaveProperty('view');
48-
expect(tools).toHaveProperty('search_content');
49-
expect(tools).toHaveProperty('find_files');
50-
expect(tools).toHaveProperty('execute_command');
47+
// Built-in non-workspace tools should still be present
48+
expect(tools).toHaveProperty('request_sandbox_access');
5149
});
5250

5351
it('should not overwrite built-in tools with extraTools of the same name', () => {
5452
const sneakyTool = createTool({
55-
id: 'view',
56-
description: 'Trying to overwrite the built-in view tool',
53+
id: 'request_sandbox_access',
54+
description: 'Trying to overwrite the built-in request_sandbox_access tool',
5755
inputSchema: z.object({}),
5856
execute: async () => ({ result: 'sneaky' }),
5957
});
6058

61-
const getDynamicTools = createDynamicTools(undefined, { view: sneakyTool });
59+
const getDynamicTools = createDynamicTools(undefined, { request_sandbox_access: sneakyTool });
6260
const tools = getDynamicTools({ requestContext: makeRequestContext() });
6361

64-
// Built-in view should NOT be replaced by the extra tool
65-
expect(tools.view).not.toBe(sneakyTool);
62+
// Built-in request_sandbox_access should NOT be replaced by the extra tool
63+
expect(tools.request_sandbox_access).not.toBe(sneakyTool);
6664
});
6765

6866
it('should return extraTools even when no MCP manager is provided', () => {
@@ -90,9 +88,9 @@ describe('createDynamicTools – extraTools', () => {
9088
const getDynamicTools = createDynamicTools(undefined, undefined);
9189
const tools = getDynamicTools({ requestContext: makeRequestContext() });
9290

93-
// Should have built-in tools but nothing extra
94-
expect(tools).toHaveProperty('view');
95-
expect(tools).toHaveProperty('search_content');
91+
// Should have built-in non-workspace tools but nothing extra
92+
// Note: workspace tools (view, search_content, etc.) are provided by the workspace, not createDynamicTools
93+
expect(tools).toHaveProperty('request_sandbox_access');
9694
expect(tools).not.toHaveProperty('my_custom_tool');
9795
});
9896
});
@@ -105,10 +103,10 @@ describe('getToolCategory – extra tools', () => {
105103
});
106104

107105
it('should still categorize built-in tools correctly', () => {
108-
expect(getToolCategory('view')).toBe('read');
109-
expect(getToolCategory('search_content')).toBe('read');
110-
expect(getToolCategory('string_replace_lsp')).toBe('edit');
111-
expect(getToolCategory('execute_command')).toBe('execute');
106+
expect(getToolCategory(MC_TOOLS.VIEW)).toBe('read');
107+
expect(getToolCategory(MC_TOOLS.SEARCH_CONTENT)).toBe('read');
108+
expect(getToolCategory(MC_TOOLS.STRING_REPLACE_LSP)).toBe('edit');
109+
expect(getToolCategory(MC_TOOLS.EXECUTE_COMMAND)).toBe('execute');
112110
});
113111

114112
it('should return null for always-allowed tools', () => {
@@ -123,31 +121,33 @@ describe('createDynamicTools – denied tool filtering', () => {
123121
const getDynamicTools = createDynamicTools();
124122
const tools = getDynamicTools({
125123
requestContext: makeRequestContext({
126-
permissionRules: { categories: {}, tools: { execute_command: 'deny' } },
124+
permissionRules: { categories: {}, tools: { request_sandbox_access: 'deny' } },
127125
}),
128126
});
129127

130-
expect(tools).not.toHaveProperty('execute_command');
131-
// Other tools should still be present
132-
expect(tools).toHaveProperty('view');
133-
expect(tools).toHaveProperty('search_content');
128+
expect(tools).not.toHaveProperty('request_sandbox_access');
134129
});
135130

136131
it('should omit multiple denied tools', () => {
137-
const getDynamicTools = createDynamicTools();
132+
const myTool = createTool({
133+
id: 'my_tool',
134+
description: 'A custom tool',
135+
inputSchema: z.object({}),
136+
execute: async () => ({ result: 'custom' }),
137+
});
138+
139+
const getDynamicTools = createDynamicTools(undefined, { my_tool: myTool });
138140
const tools = getDynamicTools({
139141
requestContext: makeRequestContext({
140142
permissionRules: {
141143
categories: {},
142-
tools: { execute_command: 'deny', view: 'deny', find_files: 'deny' },
144+
tools: { request_sandbox_access: 'deny', my_tool: 'deny' },
143145
},
144146
}),
145147
});
146148

147-
expect(tools).not.toHaveProperty('execute_command');
148-
expect(tools).not.toHaveProperty('view');
149-
expect(tools).not.toHaveProperty('find_files');
150-
expect(tools).toHaveProperty('search_content');
149+
expect(tools).not.toHaveProperty('request_sandbox_access');
150+
expect(tools).not.toHaveProperty('my_tool');
151151
});
152152

153153
it('should keep tools with allow or ask policies', () => {
@@ -156,13 +156,12 @@ describe('createDynamicTools – denied tool filtering', () => {
156156
requestContext: makeRequestContext({
157157
permissionRules: {
158158
categories: {},
159-
tools: { execute_command: 'allow', view: 'ask' },
159+
tools: { request_sandbox_access: 'allow' },
160160
},
161161
}),
162162
});
163163

164-
expect(tools).toHaveProperty('execute_command');
165-
expect(tools).toHaveProperty('view');
164+
expect(tools).toHaveProperty('request_sandbox_access');
166165
});
167166

168167
it('should also deny extraTools when they have a deny policy', () => {
@@ -187,32 +186,32 @@ describe('createDynamicTools – denied tool filtering', () => {
187186
describe('buildToolGuidance – denied tool filtering', () => {
188187
it('should omit guidance for denied tools', () => {
189188
const guidance = buildToolGuidance('build', {
190-
deniedTools: new Set(['execute_command']),
189+
deniedTools: new Set([MC_TOOLS.EXECUTE_COMMAND]),
191190
});
192191

193-
expect(guidance).not.toContain('**execute_command**');
194-
expect(guidance).toContain('**view**');
195-
expect(guidance).toContain('**search_content**');
192+
expect(guidance).not.toContain(`**${MC_TOOLS.EXECUTE_COMMAND}**`);
193+
expect(guidance).toContain(`**${MC_TOOLS.VIEW}**`);
194+
expect(guidance).toContain(`**${MC_TOOLS.SEARCH_CONTENT}**`);
196195
});
197196

198197
it('should omit multiple denied tools from guidance', () => {
199198
const guidance = buildToolGuidance('build', {
200-
deniedTools: new Set(['execute_command', 'write_file', 'subagent']),
199+
deniedTools: new Set([MC_TOOLS.EXECUTE_COMMAND, MC_TOOLS.WRITE_FILE, 'subagent']),
201200
});
202201

203-
expect(guidance).not.toContain('**execute_command**');
204-
expect(guidance).not.toContain('**write_file**');
202+
expect(guidance).not.toContain(`**${MC_TOOLS.EXECUTE_COMMAND}**`);
203+
expect(guidance).not.toContain(`**${MC_TOOLS.WRITE_FILE}**`);
205204
expect(guidance).not.toContain('**subagent**');
206-
expect(guidance).toContain('**view**');
207-
expect(guidance).toContain('**string_replace_lsp**');
205+
expect(guidance).toContain(`**${MC_TOOLS.VIEW}**`);
206+
expect(guidance).toContain(`**${MC_TOOLS.STRING_REPLACE_LSP}**`);
208207
});
209208

210209
it('should include all tools when no denied set is provided', () => {
211210
const guidance = buildToolGuidance('build');
212211

213-
expect(guidance).toContain('**execute_command**');
214-
expect(guidance).toContain('**view**');
215-
expect(guidance).toContain('**string_replace_lsp**');
212+
expect(guidance).toContain(`**${MC_TOOLS.EXECUTE_COMMAND}**`);
213+
expect(guidance).toContain(`**${MC_TOOLS.VIEW}**`);
214+
expect(guidance).toContain(`**${MC_TOOLS.STRING_REPLACE_LSP}**`);
216215
expect(guidance).toContain('**subagent**');
217216
});
218217
});

mastracode/src/agents/prompts/tool-guidance.ts

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
* and are scoped to what's available in the current mode.
55
*/
66

7+
import { MC_TOOLS } from '../../tool-names.js';
8+
79
interface ToolGuidanceOptions {
810
hasWebSearch?: boolean;
911
/** Tool names that have been denied — omit their guidance sections. */
@@ -24,44 +26,44 @@ You have access to the following tools. Use the RIGHT tool for the job:`);
2426

2527
const readTools: string[] = [];
2628

27-
if (!denied.has('view')) {
29+
if (!denied.has(MC_TOOLS.VIEW)) {
2830
readTools.push(`
29-
**view** — Read file contents or list directories
31+
**${MC_TOOLS.VIEW}** — Read file contents or list directories
3032
- Use this to read files before editing them. NEVER propose changes to code you haven't read.
3133
- Use \`view_range\` for large files to read specific sections.
3234
- For directory listings, this shows 2 levels deep.
3335
- Example: To check lines 50-100 of a large file: \`view("src/big-file.ts", { view_range: [50, 100] })\``);
3436
}
3537

36-
if (!denied.has('search_content')) {
38+
if (!denied.has(MC_TOOLS.SEARCH_CONTENT)) {
3739
readTools.push(`
38-
**search_content** — Search file contents using regex
40+
**${MC_TOOLS.SEARCH_CONTENT}** — Search file contents using regex
3941
- Use this for ALL content search (finding functions, variables, error messages, imports, etc.)
4042
- NEVER use \`execute_command\` with grep, rg, or ag. Always use the search_content tool.
4143
- Supports regex patterns, file type filtering, and context lines.
4244
- Example: Find where a function is defined: \`search_content("function handleSubmit", { glob: "**/*.ts" })\`
4345
- Example: Find all imports of a module: \`search_content("from ['\\"\\]express['\\"\\]", { glob: "**/*.ts" })\``);
4446
}
4547

46-
if (!denied.has('find_files')) {
48+
if (!denied.has(MC_TOOLS.FIND_FILES)) {
4749
readTools.push(`
48-
**find_files** — Find files by name pattern
50+
**${MC_TOOLS.FIND_FILES}** — Find files by name pattern
4951
- Use this to find files matching a pattern (e.g., "**/*.ts", "src/**/test*").
5052
- NEVER use \`execute_command\` with find or ls for file search. Always use find_files.
5153
- Respects .gitignore automatically.
5254
- Example: Find all test files: \`find_files("**/*.test.ts")\`
5355
- Example: Find config files: \`find_files("**/config.{js,ts,json}")\``);
5456
}
5557

56-
if (!denied.has('execute_command')) {
58+
if (!denied.has(MC_TOOLS.EXECUTE_COMMAND)) {
5759
readTools.push(`
58-
**execute_command** — Run shell commands
60+
**${MC_TOOLS.EXECUTE_COMMAND}** — Run shell commands
5961
- Use for: git, npm/pnpm, docker, build tools, test runners, and other terminal operations.
60-
- Do NOT use for: file reading (use view), file search (use search_content/find_files), file editing (use string_replace_lsp/write_file).
62+
- Do NOT use for: file reading (use ${MC_TOOLS.VIEW}), file search (use ${MC_TOOLS.SEARCH_CONTENT}/${MC_TOOLS.FIND_FILES}), file editing (use ${MC_TOOLS.STRING_REPLACE_LSP}/${MC_TOOLS.WRITE_FILE}).
6163
- Commands have a 30-second default timeout. Use the \`timeout\` parameter for longer-running commands.
6264
- Pipe to \`| tail -N\` for commands with long output — the full output streams to the user, only the last N lines are returned to you. If you're building any kind of package you should be tailing.
6365
- Good: Run independent commands in parallel when possible.
64-
- Bad: Running \`cat file.txt\` — use the view tool instead.`);
66+
- Bad: Running \`cat file.txt\` — use the ${MC_TOOLS.VIEW} tool instead.`);
6567
}
6668

6769
if (readTools.length > 0) {
@@ -73,22 +75,22 @@ You have access to the following tools. Use the RIGHT tool for the job:`);
7375
if (modeId !== 'plan') {
7476
const writeTools: string[] = [];
7577

76-
if (!denied.has('string_replace_lsp')) {
78+
if (!denied.has(MC_TOOLS.STRING_REPLACE_LSP)) {
7779
writeTools.push(`
78-
**string_replace_lsp** — Edit files by replacing exact text
79-
- You MUST read a file with \`view\` before editing it.
80+
**${MC_TOOLS.STRING_REPLACE_LSP}** — Edit files by replacing exact text
81+
- You MUST read a file with \`${MC_TOOLS.VIEW}\` before editing it.
8082
- \`old_str\` must be an exact match of existing text in the file.
8183
- Provide enough surrounding context in \`old_str\` to make it unique.
82-
- For creating new files, use \`write_file\` instead.
84+
- For creating new files, use \`${MC_TOOLS.WRITE_FILE}\` instead.
8385
- Good: Include 2-3 lines of surrounding context to ensure uniqueness.
8486
- Bad: Using just \`return true;\` — too common, will match multiple places.`);
8587
}
8688

87-
if (!denied.has('write_file')) {
89+
if (!denied.has(MC_TOOLS.WRITE_FILE)) {
8890
writeTools.push(`
89-
**write_file** — Create new files or overwrite existing ones
91+
**${MC_TOOLS.WRITE_FILE}** — Create new files or overwrite existing ones
9092
- Use this to create new files.
91-
- If overwriting an existing file, you MUST have read it first with \`view\`.
93+
- If overwriting an existing file, you MUST have read it first with \`${MC_TOOLS.VIEW}\`.
9294
- NEVER create files unless necessary. Prefer editing existing files.`);
9395
}
9496

mastracode/src/agents/subagents/audit-tests.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* explores the repo's existing testing conventions, and produces
66
* a detailed audit report with actionable improvement recommendations.
77
*/
8+
import { MC_TOOLS } from '../../tool-names.js';
89
import type { SubagentDefinition } from './types.js';
910

1011
export const auditTestsSubagent: SubagentDefinition = {
@@ -117,5 +118,5 @@ Prioritized, actionable list. Most impactful improvements first. Be specific —
117118
- Ground all feedback in the repo's actual conventions, not generic best practices.
118119
- Be direct. If tests are sloppy, say so. If they're good, say that too.
119120
- Focus on **actionable feedback** — every finding should have a clear "do this instead" recommendation.`,
120-
allowedTools: ['view', 'search_content', 'find_files'],
121+
allowedTools: [MC_TOOLS.VIEW, MC_TOOLS.SEARCH_CONTENT, MC_TOOLS.FIND_FILES],
121122
};

mastracode/src/agents/subagents/execute.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* read and write tools to complete it. It can modify files, run commands,
66
* and perform actual development work within a constrained scope.
77
*/
8+
import { MC_TOOLS } from '../../tool-names.js';
89
import type { SubagentDefinition } from './types.js';
910

1011
export const executeSubagent: SubagentDefinition = {
@@ -45,14 +46,14 @@ End with a structured summary:
4546
. **Notes**: Follow-up needed (if any)`,
4647
allowedTools: [
4748
// Read tools
48-
'view',
49-
'search_content',
50-
'find_files',
49+
MC_TOOLS.VIEW,
50+
MC_TOOLS.SEARCH_CONTENT,
51+
MC_TOOLS.FIND_FILES,
5152
// Write tools
52-
'string_replace_lsp',
53-
'write_file',
53+
MC_TOOLS.STRING_REPLACE_LSP,
54+
MC_TOOLS.WRITE_FILE,
5455
// Execution tool
55-
'execute_command',
56+
MC_TOOLS.EXECUTE_COMMAND,
5657
// Task tracking (built-in harness tools)
5758
'task_write',
5859
'task_check',

mastracode/src/agents/subagents/explore.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* "understand how module Y works") and uses read-only tools to explore
66
* the codebase, then returns a concise summary of its findings.
77
*/
8+
import { MC_TOOLS } from '../../tool-names.js';
89
import type { SubagentDefinition } from './types.js';
910

1011
export const exploreSubagent: SubagentDefinition = {
@@ -36,5 +37,5 @@ End with a structured summary:
3637
. **Details**: Additional context if needed
3738
3839
Keep your summary under 300 words.`,
39-
allowedTools: ['view', 'search_content', 'find_files'],
40+
allowedTools: [MC_TOOLS.VIEW, MC_TOOLS.SEARCH_CONTENT, MC_TOOLS.FIND_FILES],
4041
};

mastracode/src/agents/subagents/plan.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* implementation plan. It can read the codebase to understand existing
66
* patterns and architecture, but cannot modify anything.
77
*/
8+
import { MC_TOOLS } from '../../tool-names.js';
89
import type { SubagentDefinition } from './types.js';
910

1011
export const planSubagent: SubagentDefinition = {
@@ -38,5 +39,5 @@ Structure your plan as:
3839
. **Risks**: Potential issues or edge cases (if any)
3940
4041
Be specific about code locations (file paths, function names, line numbers). Keep the plan actionable and under 500 words.`,
41-
allowedTools: ['view', 'search_content', 'find_files'],
42+
allowedTools: [MC_TOOLS.VIEW, MC_TOOLS.SEARCH_CONTENT, MC_TOOLS.FIND_FILES],
4243
};

0 commit comments

Comments
 (0)