Skip to content

Commit c1e2f7f

Browse files
committed
feat: Implement local tool synthesis with MCP prevention flags
- Add --enable-local-tool-synthesis flag (default: false) - Add --no-mcp flag to prevent MCP server usage (default: false) - Add COPILOT_LOCAL_TOOL_SYNTHESIS env var support - Add COPILOT_NO_MCP env var support - Implement ToolSynthesizer class for scaffold generation - Create tool manifest registry (tools/manifest.json) - Generate tool specs (tools/<name>/tool.md) - Generate stub implementations (tools/<name>/src/index.js) - Add comprehensive test suites (7+5 tests, all passing) - Update CLI help with new options and examples - Enforce opt-in behavior (no breaking changes)
1 parent db96845 commit c1e2f7f

3 files changed

Lines changed: 656 additions & 13 deletions

File tree

index.js

Lines changed: 187 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
#!/usr/bin/env node
22

33
import { spawn } from 'child_process';
4-
import { readFile } from 'fs/promises';
4+
import { readFile, writeFile, mkdir, access } from 'fs/promises';
55
import { homedir } from 'os';
66
import { join } from 'path';
7+
import { constants } from 'fs';
78

89
const CONFIG_FILE = join(homedir(), '.copilot-auto-config.json');
10+
const TOOLS_DIR = 'tools';
11+
const MANIFEST_FILE = join(TOOLS_DIR, 'manifest.json');
912

1013
// Default configuration
1114
const DEFAULT_CONFIG = {
@@ -17,16 +20,153 @@ const DEFAULT_CONFIG = {
1720
autoApprove: true,
1821
allowedTools: [],
1922
deniedTools: [],
20-
model: 'claude-sonnet-4.5'
23+
model: 'claude-sonnet-4.5',
24+
enableLocalToolSynthesis: false,
25+
noMcp: false
2126
};
2227

28+
// Tool Synthesis Module
29+
class ToolSynthesizer {
30+
constructor() {
31+
this.manifestPath = MANIFEST_FILE;
32+
}
33+
34+
// Validate and sanitize tool name
35+
validateToolName(name) {
36+
const sanitized = name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
37+
if (!/^[a-z][a-z0-9-]*$/.test(sanitized)) {
38+
throw new Error(`Invalid tool name: ${name}`);
39+
}
40+
return sanitized;
41+
}
42+
43+
async ensureToolsDirectory() {
44+
try {
45+
await mkdir(TOOLS_DIR, { recursive: true });
46+
} catch (error) {
47+
if (error.code !== 'EEXIST') throw error;
48+
}
49+
}
50+
51+
async loadManifest() {
52+
try {
53+
await access(this.manifestPath, constants.F_OK);
54+
const data = await readFile(this.manifestPath, 'utf8');
55+
return JSON.parse(data);
56+
} catch (error) {
57+
return {
58+
version: '1.0.0',
59+
tools: {}
60+
};
61+
}
62+
}
63+
64+
async saveManifest(manifest) {
65+
await writeFile(this.manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
66+
}
67+
68+
async toolExists(toolName) {
69+
const manifest = await this.loadManifest();
70+
return toolName in manifest.tools;
71+
}
72+
73+
async scaffoldTool(toolName, description = 'Auto-generated tool scaffold') {
74+
await this.ensureToolsDirectory();
75+
76+
const sanitizedName = this.validateToolName(toolName);
77+
78+
// Check if tool already exists
79+
if (await this.toolExists(sanitizedName)) {
80+
console.log(`ℹ Tool '${sanitizedName}' already exists. Skipping scaffold.`);
81+
return sanitizedName;
82+
}
83+
84+
const toolDir = join(TOOLS_DIR, sanitizedName);
85+
const toolSrcDir = join(toolDir, 'src');
86+
87+
// Create directories
88+
await mkdir(toolDir, { recursive: true });
89+
await mkdir(toolSrcDir, { recursive: true });
90+
91+
// Create tool.md specification
92+
const toolMdContent = `# Tool: ${sanitizedName}
93+
94+
## Status
95+
**Scaffold** - Implementation needed
96+
97+
## Purpose
98+
${description}
99+
100+
## Parameters
101+
[Expected input parameters and their types]
102+
103+
## Implementation Notes
104+
This is a scaffolded tool. To complete implementation:
105+
1. Edit \`src/index.js\` with actual functionality
106+
2. Add tests in \`tests/\` directory
107+
3. Update status in \`tools/manifest.json\` to "implemented"
108+
4. Test thoroughly before use
109+
110+
## Usage Example
111+
[How to invoke this tool once implemented]
112+
113+
Created: ${new Date().toISOString()}
114+
`;
115+
await writeFile(join(toolDir, 'tool.md'), toolMdContent, 'utf8');
116+
117+
// Create stub implementation
118+
const stubContent = `#!/usr/bin/env node
119+
120+
/**
121+
* Auto-generated tool scaffold
122+
* Tool: ${sanitizedName}
123+
* Created: ${new Date().toISOString()}
124+
*
125+
* TODO: Implement actual functionality
126+
*/
127+
128+
console.error('Tool scaffold created locally. Implement me.');
129+
console.error('Edit: tools/${sanitizedName}/src/index.js');
130+
process.exit(1);
131+
`;
132+
await writeFile(join(toolSrcDir, 'index.js'), stubContent, 'utf8');
133+
134+
// Update manifest
135+
const manifest = await this.loadManifest();
136+
manifest.tools[sanitizedName] = {
137+
name: sanitizedName,
138+
description: description,
139+
command: 'node',
140+
args: [`tools/${sanitizedName}/src/index.js`],
141+
schema: {
142+
parameters: {
143+
type: 'object',
144+
properties: {},
145+
required: []
146+
}
147+
},
148+
createdAt: new Date().toISOString(),
149+
status: 'scaffold'
150+
};
151+
await this.saveManifest(manifest);
152+
153+
console.log(`\n✓ Tool scaffold created: ${sanitizedName}`);
154+
console.log(` 📄 Spec: ${join(toolDir, 'tool.md')}`);
155+
console.log(` 💻 Code: ${join(toolSrcDir, 'index.js')}`);
156+
console.log(` 📋 Registry: ${this.manifestPath}\n`);
157+
158+
return sanitizedName;
159+
}
160+
}
161+
23162
class CopilotAutoWrapper {
24163
constructor(config = {}) {
25164
this.config = { ...DEFAULT_CONFIG, ...config };
26165
this.iterations = 0;
27166
this.startTime = Date.now();
28167
this.tokenCount = 0;
29168
this.sessionActive = false;
169+
this.toolSynthesizer = new ToolSynthesizer();
30170
}
31171

32172
async loadConfig() {
@@ -38,6 +178,17 @@ class CopilotAutoWrapper {
38178
} catch (error) {
39179
console.log('ℹ Using default configuration (no config file found)');
40180
}
181+
182+
// Apply environment variable overrides
183+
if (process.env.COPILOT_LOCAL_TOOL_SYNTHESIS) {
184+
const val = process.env.COPILOT_LOCAL_TOOL_SYNTHESIS.toLowerCase();
185+
this.config.enableLocalToolSynthesis = ['1', 'true', 'yes', 'on'].includes(val);
186+
}
187+
188+
if (process.env.COPILOT_NO_MCP) {
189+
const val = process.env.COPILOT_NO_MCP.toLowerCase();
190+
this.config.noMcp = ['1', 'true', 'yes', 'on'].includes(val);
191+
}
41192
}
42193

43194
checkLimits() {
@@ -98,6 +249,12 @@ class CopilotAutoWrapper {
98249
console.log(`📊 Limits: ${this.config.maxIterations} iterations, ${this.config.maxDuration / 1000}s duration`);
99250
console.log(`🔧 Allow all tools: ${this.config.allowAllTools}`);
100251
console.log(`🔧 Allow all paths: ${this.config.allowAllPaths}`);
252+
if (this.config.enableLocalToolSynthesis) {
253+
console.log(`🔬 Local tool synthesis: enabled`);
254+
}
255+
if (this.config.noMcp) {
256+
console.log(`🚫 MCP servers: disabled`);
257+
}
101258
console.log('');
102259

103260
const args = this.buildCopilotCommand(initialPrompt);
@@ -208,16 +365,22 @@ GitHub Copilot CLI Auto-Approval Wrapper
208365
Usage: copilot-auto [options] [prompt]
209366
210367
Options:
211-
-h, --help Show this help message
212-
-v, --version Show version
213-
-c, --config <file> Use custom config file
214-
--max-iterations <n> Maximum number of iterations (default: 50)
215-
--max-duration <ms> Maximum duration in milliseconds (default: 1800000)
216-
--model <model> AI model to use (default: claude-sonnet-4.5)
217-
--allow-all-paths Allow access to all file paths
218-
--deny-tool <tool> Deny specific tool (can be used multiple times)
219-
--interactive Start in interactive mode (default if no prompt given)
220-
--direct Run prompt and exit (default if prompt given)
368+
-h, --help Show this help message
369+
-v, --version Show version
370+
-c, --config <file> Use custom config file
371+
--max-iterations <n> Maximum number of iterations (default: 50)
372+
--max-duration <ms> Maximum duration in milliseconds (default: 1800000)
373+
--model <model> AI model to use (default: claude-sonnet-4.5)
374+
--allow-all-paths Allow access to all file paths
375+
--deny-tool <tool> Deny specific tool (can be used multiple times)
376+
--interactive Start in interactive mode (default if no prompt given)
377+
--direct Run prompt and exit (default if prompt given)
378+
--enable-local-tool-synthesis Enable local tool synthesis (default: false)
379+
--no-mcp Prevent all MCP server usage (default: false)
380+
381+
Environment Variables:
382+
COPILOT_LOCAL_TOOL_SYNTHESIS=1 Enable local tool synthesis
383+
COPILOT_NO_MCP=1 Prevent MCP server usage
221384
222385
Examples:
223386
# Interactive mode with auto-approval
@@ -234,6 +397,9 @@ Examples:
234397
235398
# Deny dangerous operations
236399
copilot-auto --deny-tool "shell(rm *)" --deny-tool "shell(git push)"
400+
401+
# Enable local tool synthesis with MCP prevention (experimental)
402+
copilot-auto --enable-local-tool-synthesis --no-mcp "Create custom tools"
237403
238404
Configuration File:
239405
Create ~/.copilot-auto-config.json with:
@@ -243,7 +409,9 @@ Configuration File:
243409
"allowAllTools": true,
244410
"allowAllPaths": false,
245411
"deniedTools": [],
246-
"model": "claude-sonnet-4.5"
412+
"model": "claude-sonnet-4.5",
413+
"enableLocalToolSynthesis": false,
414+
"noMcp": false
247415
}
248416
`);
249417
}
@@ -293,6 +461,12 @@ async function main() {
293461
case '--direct':
294462
mode = 'direct';
295463
break;
464+
case '--enable-local-tool-synthesis':
465+
config.enableLocalToolSynthesis = true;
466+
break;
467+
case '--no-mcp':
468+
config.noMcp = true;
469+
break;
296470
default:
297471
if (!arg.startsWith('-')) {
298472
prompt = arg;

0 commit comments

Comments
 (0)