Skip to content

Commit e59edac

Browse files
committed
approval specific tools with --tools
1 parent f408154 commit e59edac

File tree

3 files changed

+76
-48
lines changed

3 files changed

+76
-48
lines changed

src/commands/add.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,28 @@ export default class Add extends Command {
66
static description = 'Add a server to a client'
77

88
static flags = {
9-
client: Flags.string({char: 'c', description: 'client name', required: true}),
9+
client: Flags.string({char: 'c', description: 'Client name to add the server to', required: true}),
10+
tools: Flags.string({
11+
char: 't',
12+
description: 'Comma separated list of approved tools'
13+
})
1014
}
1115

1216
static args = {
13-
mcpServer: Args.string({description: 'MCP server to install', required: true}),
17+
server: Args.string({description: 'The mcp server to add', required: true}),
1418
}
1519

1620
async run() {
1721
const {args, flags} = await this.parse(Add)
18-
const mcpServer = args.mcpServer
22+
const server = args.server
1923
const client = flags.client
2024

25+
var approvedTools = '';
26+
27+
if (flags.tools) {
28+
approvedTools += '--tools ' + flags.tools
29+
}
30+
2131
// Determine the configuration file path based on the OS
2232
const configFilePath = process.platform === 'win32'
2333
? path.join(process.env.APPDATA || '', 'Claude', 'claude_desktop_config.json')
@@ -38,26 +48,27 @@ export default class Add extends Command {
3848
}
3949

4050
// Check if the server already exists
41-
if (config.mcpServers[mcpServer]) {
42-
this.log(`Server ${mcpServer} already exists in the configuration for client ${client}`)
51+
if (config.mcpServers[server]) {
52+
this.log(`Server ${server} already exists in the configuration for client ${client}`)
4353
return
4454
}
4555

4656
// Add the new mcp-server
47-
config.mcpServers[mcpServer] = {
57+
config.mcpServers[server] = {
4858
command: 'npx',
4959
args: [
5060
"-y",
51-
"@mcpgod/cli",
61+
"mcpgod",
5262
"run",
53-
mcpServer
63+
server,
64+
approvedTools
5465
]
5566
}
5667

5768
// Write the updated configuration back to the file
5869
try {
5970
fs.writeFileSync(configFilePath, JSON.stringify(config, null, 2))
60-
this.log(`Successfully added ${mcpServer} to the configuration for client ${client}`)
71+
this.log(`Successfully added ${server} to the configuration for client ${client}`)
6172
} catch (error: unknown) {
6273
if (error instanceof Error) {
6374
this.error(`Failed to write to configuration file: ${error.message}`)

src/commands/list.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import {Command, Flags, Args} from '@oclif/core'
22
import * as fs from 'fs'
33
import * as path from 'path'
4+
import {stdout, colorize} from '@oclif/core/ux'
5+
6+
interface ServerDetails {
7+
command: string;
8+
args: string[];
9+
}
10+
11+
interface Config {
12+
mcpServers?: Record<string, ServerDetails>;
13+
}
414

515
export default class List extends Command {
616
static description = 'List all the servers for a client'
@@ -31,7 +41,7 @@ export default class List extends Command {
3141
}
3242

3343
// Load the configuration file
34-
let config
44+
let config: Config
3545
try {
3646
const configFile = fs.readFileSync(configFilePath, 'utf-8')
3747
config = JSON.parse(configFile)
@@ -50,8 +60,10 @@ export default class List extends Command {
5060
this.log(`No servers found for client ${client}`)
5161
} else {
5262
for (const [serverName, serverDetails] of Object.entries(servers)) {
53-
this.log(`${serverName}`)
63+
const commandString = `${serverDetails.command} ${serverDetails.args.join(' ')}`;
64+
65+
stdout(colorize('magenta', serverName) + `: ${commandString}`);
5466
}
5567
}
5668
}
57-
}
69+
}

src/commands/run.ts

Lines changed: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Command } from '@oclif/core'
1+
import { Command, Flags } from '@oclif/core'
22
import * as fs from 'fs'
33
import * as path from 'path'
44
import { spawn, ChildProcessWithoutNullStreams } from 'child_process'
@@ -18,29 +18,45 @@ export default class Run extends Command {
1818
]
1919
static strict = false // Allow variable arguments
2020

21+
static flags = {
22+
tools: Flags.string({
23+
char: 't',
24+
description: 'Comma separated list of approved tools',
25+
default: ''
26+
})
27+
};
28+
2129
async run(): Promise<void> {
22-
const { argv } = await this.parse(Run)
30+
const { argv, flags } = await this.parse(Run)
2331
if (argv.length === 0) {
2432
this.error('Please specify a package to run')
2533
}
2634
// Assert that argv is a string array.
2735
const stringArgs = argv as string[]
2836

37+
const approvedTools = flags.tools.split(',').map(tool => tool.trim());
38+
2939
// Determine home directory for cross-platform compatibility.
3040
const homeDir = process.env.HOME || process.env.USERPROFILE;
3141
if (!homeDir) {
3242
throw new Error('Unable to determine home directory.');
3343
}
3444

35-
// Set the static log directory path: $HOME/god/logs
45+
// Set the static log directory path: $HOME/mcpgod/logs
3646
const logsDir = path.join(homeDir, 'mcpgod', 'logs');
3747
if (!fs.existsSync(logsDir)) {
3848
fs.mkdirSync(logsDir, { recursive: true }); // Ensure nested directories are created.
3949
}
4050

51+
function sanitizeFilename(str: string): string {
52+
// Replace invalid characters with an underscore
53+
return str.replace(/[\/\\?%*:|"<>]/g, '_');
54+
}
55+
4156
// Create log file with timestamp.
4257
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
43-
const logFile = path.join(logsDir, `npx-run-${timestamp}.log`)
58+
const filename = sanitizeFilename(`run--${stringArgs.join(' ')}--${timestamp}.log`);
59+
const logFile = path.join(logsDir, filename)
4460

4561
// Setup Winston logger.
4662
const logger = winston.createLogger({
@@ -54,7 +70,8 @@ export default class Run extends Command {
5470
transports: [new winston.transports.File({ filename: logFile })]
5571
})
5672

57-
logger.info(`Command: npx -y ${stringArgs.join(' ')}`)
73+
logger.info('')
74+
logger.info(`Command: ${stringArgs.join(' ')}`)
5875
logger.info(`Started at: ${new Date().toISOString()}`)
5976
logger.info('')
6077

@@ -66,41 +83,24 @@ export default class Run extends Command {
6683
filteredEnv.TERM = filteredEnv.TERM || 'xterm-color'
6784

6885
function handleOutput(data: string, stream: NodeJS.WritableStream) {
69-
const approvedTools = ["echo", "add"];
70-
71-
//const trimmedData = data.trim();
72-
73-
// Only attempt JSON parsing if the data appears to be a JSON object
74-
//if (trimmedData.startsWith("{") && trimmedData.endsWith("}")) {
75-
try {
76-
//const parsed = JSON.parse(trimmedData);
77-
const parsed = JSON.parse(data);
78-
79-
// Check if parsed JSON has the expected result.tools structure before filtering
80-
if (parsed && parsed.result && Array.isArray(parsed.result.tools)) {
81-
parsed.result.tools = parsed.result.tools.filter(
82-
(tool: { name: string }) => approvedTools.includes(tool.name)
83-
);
84-
85-
const jsonString = JSON.stringify(parsed);
86-
87-
// apparently claude desktop expects a newline at the end.
88-
data = jsonString + "\n";
89-
}
90-
} catch (error) {
91-
// If JSON parsing fails, fall back to treating the data as plain text.
86+
try {
87+
const parsed = JSON.parse(data);
88+
// Check if parsed JSON has the expected result.tools structure before filtering
89+
if (parsed && parsed.result && Array.isArray(parsed.result.tools)) {
90+
parsed.result.tools = parsed.result.tools.filter(
91+
(tool: { name: string }) => approvedTools.includes(tool.name)
92+
);
93+
const jsonString = JSON.stringify(parsed);
94+
// Apparently claude desktop expects a newline at the end.
95+
data = jsonString + "\n";
9296
}
93-
//}
94-
97+
} catch (error) {
98+
// If JSON parsing fails, fall back to treating the data as plain text.
99+
}
95100
stream.write(data);
96101
const cleaned = removeControlChars(stripAnsi(data));
97102
logger.info(cleaned);
98103
}
99-
100-
101-
102-
103-
104104

105105
// Helper to forward stdin to the child process.
106106
// writeFn should be a function accepting a string.
@@ -121,12 +121,17 @@ export default class Run extends Command {
121121
childCommand = 'npx'
122122
}
123123
const childArgs = ['-y', ...stringArgs]
124+
const shell = true; //process.stdout.isTTY ? true : false;
125+
126+
logger.info(`Spawn: ${childCommand} ${childArgs.join(' ')}`)
127+
logger.info(`Shell: ${shell}`)
128+
logger.info('')
124129

125130
const child = spawn(childCommand, childArgs, {
126131
cwd: process.cwd(),
127132
env: filteredEnv,
128133
stdio: ['pipe', 'pipe', 'pipe'],
129-
shell: process.stdout.isTTY
134+
shell: shell
130135
}) as ChildProcessWithoutNullStreams
131136

132137
child.stdout.on('data', (data: Buffer) => {

0 commit comments

Comments
 (0)