-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add list-tools command for standalone tool discovery #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| name: CI | ||
|
|
||
| on: | ||
| push: | ||
| branches: [ main ] | ||
| pull_request: | ||
| branches: [ main ] | ||
|
|
||
| jobs: | ||
| test: | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Setup Bun | ||
| uses: oven-sh/setup-bun@v2 | ||
| with: | ||
| bun-version: latest | ||
|
|
||
| - name: Install dependencies | ||
| run: bun install | ||
|
|
||
| - name: Type check | ||
| run: bun run typecheck | ||
|
|
||
| - name: Lint | ||
| run: bun run lint | ||
|
|
||
| - name: Run tests | ||
| run: bun run test | ||
|
|
||
| - name: Build executable | ||
| run: bun run build | ||
|
|
||
| - name: Verify executable | ||
| run: | | ||
| ls -la mcp-controller | ||
| # Test that executable runs and shows usage (exits with error code 1 when no args) | ||
| ./mcp-controller || [ $? -eq 1 ] |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,18 +1,81 @@ | ||||||||||
| #!/usr/bin/env bun | ||||||||||
|
|
||||||||||
| import { McpProxyServer } from './proxy-server.js'; | ||||||||||
| import type { ProxyConfig } from './types.js'; | ||||||||||
| import type { ProxyConfig, Tool } from './types.js'; | ||||||||||
| import { TargetServerManager } from './target-server.js'; | ||||||||||
|
|
||||||||||
| function parseListToolsArguments(args: string[]): ProxyConfig { | ||||||||||
| if (args.length === 0) { | ||||||||||
| process.stderr.write('Error: No target command specified for list-tools\n'); | ||||||||||
| process.exit(1); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| let enabledTools: string[] | undefined; | ||||||||||
| let disabledTools: string[] | undefined; | ||||||||||
| const targetCommand: string[] = []; | ||||||||||
|
|
||||||||||
| for (let i = 0; i < args.length; i++) { | ||||||||||
| const arg = args[i]; | ||||||||||
|
|
||||||||||
| if (arg === '--enabled-tools') { | ||||||||||
| if (i + 1 >= args.length) { | ||||||||||
| process.stderr.write('Error: --enabled-tools requires a value\n'); | ||||||||||
| process.exit(1); | ||||||||||
| } | ||||||||||
| if (disabledTools !== undefined) { | ||||||||||
| process.stderr.write('Error: --enabled-tools and --disabled-tools are mutually exclusive\n'); | ||||||||||
| process.exit(1); | ||||||||||
| } | ||||||||||
| enabledTools = args[i + 1].split(',').map(tool => tool.trim()).filter(tool => tool.length > 0); | ||||||||||
| i++; // Skip the value argument | ||||||||||
| } else if (arg === '--disabled-tools') { | ||||||||||
| if (i + 1 >= args.length) { | ||||||||||
| process.stderr.write('Error: --disabled-tools requires a value\n'); | ||||||||||
| process.exit(1); | ||||||||||
| } | ||||||||||
| if (enabledTools !== undefined) { | ||||||||||
| process.stderr.write('Error: --enabled-tools and --disabled-tools are mutually exclusive\n'); | ||||||||||
| process.exit(1); | ||||||||||
| } | ||||||||||
| disabledTools = args[i + 1].split(',').map(tool => tool.trim()).filter(tool => tool.length > 0); | ||||||||||
| i++; // Skip the value argument | ||||||||||
| } else { | ||||||||||
| targetCommand.push(arg); | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| if (targetCommand.length === 0) { | ||||||||||
| process.stderr.write('Error: No target command specified for list-tools\n'); | ||||||||||
| process.exit(1); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| return { | ||||||||||
| targetCommand, | ||||||||||
| enabledTools, | ||||||||||
| disabledTools, | ||||||||||
| serverName: 'mcp-controller', | ||||||||||
| serverVersion: '0.1.0', | ||||||||||
| mode: 'list-tools', | ||||||||||
| }; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| function parseArguments(): ProxyConfig { | ||||||||||
| const args = process.argv.slice(2); | ||||||||||
|
|
||||||||||
| if (args.length === 0) { | ||||||||||
| process.stderr.write('Usage: mcp-controller [--enabled-tools <tool1,tool2,...>] [--disabled-tools <tool1,tool2,...>] <command> [args...]\n'); | ||||||||||
| process.stderr.write(' mcp-controller list-tools [--enabled-tools <tool1,tool2,...>] [--disabled-tools <tool1,tool2,...>] <command> [args...]\n'); | ||||||||||
| process.stderr.write('Example: mcp-controller --enabled-tools add,subtract bun run server.ts\n'); | ||||||||||
| process.stderr.write('Example: mcp-controller list-tools bun run server.ts\n'); | ||||||||||
| process.stderr.write('Example: mcp-controller --disabled-tools dangerous-tool bun run server.ts\n'); | ||||||||||
| process.exit(1); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // Check if first argument is list-tools | ||||||||||
| if (args[0] === 'list-tools') { | ||||||||||
| return parseListToolsArguments(args.slice(1)); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| let enabledTools: string[] | undefined; | ||||||||||
| let disabledTools: string[] | undefined; | ||||||||||
| const targetCommand: string[] = []; | ||||||||||
|
|
@@ -58,13 +121,128 @@ function parseArguments(): ProxyConfig { | |||||||||
| disabledTools, | ||||||||||
| serverName: 'mcp-controller', | ||||||||||
| serverVersion: '0.1.0', | ||||||||||
| mode: 'proxy', | ||||||||||
| }; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| async function listTools(config: ProxyConfig): Promise<void> { | ||||||||||
| const targetManager = new TargetServerManager(); | ||||||||||
| let targetServer; | ||||||||||
|
|
||||||||||
| try { | ||||||||||
| // Start the target server | ||||||||||
| targetServer = await targetManager.startTargetServer(config); | ||||||||||
|
|
||||||||||
| // Send initialize request | ||||||||||
| const initializeRequest = { | ||||||||||
| jsonrpc: '2.0', | ||||||||||
| id: 1, | ||||||||||
| method: 'initialize', | ||||||||||
| params: { | ||||||||||
| protocolVersion: '0.1.0', | ||||||||||
| capabilities: {}, | ||||||||||
| clientInfo: { | ||||||||||
| name: config.serverName, | ||||||||||
| version: config.serverVersion, | ||||||||||
| }, | ||||||||||
| }, | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| targetServer.stdin.write(JSON.stringify(initializeRequest) + '\n'); | ||||||||||
|
|
||||||||||
| // Wait for initialize response | ||||||||||
| const reader = targetServer.stdout.getReader(); | ||||||||||
| let buffer = ''; | ||||||||||
|
|
||||||||||
| // Read initialize response | ||||||||||
| const { value: initValue } = await reader.read(); | ||||||||||
| if (!initValue) throw new Error('No response from server'); | ||||||||||
|
|
||||||||||
| buffer += new TextDecoder().decode(initValue); | ||||||||||
| const initLines = buffer.split('\n'); | ||||||||||
| const initResponse = initLines.find(line => line.trim()); | ||||||||||
| if (!initResponse) throw new Error('No valid response received'); | ||||||||||
|
|
||||||||||
| const parsedInitResponse = JSON.parse(initResponse); | ||||||||||
| if (parsedInitResponse.error) { | ||||||||||
| throw new Error(`Initialize failed: ${parsedInitResponse.error.message}`); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // Send tools/list request | ||||||||||
| const toolsListRequest = { | ||||||||||
| jsonrpc: '2.0', | ||||||||||
| id: 2, | ||||||||||
| method: 'tools/list', | ||||||||||
| params: {}, | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| targetServer.stdin.write(JSON.stringify(toolsListRequest) + '\n'); | ||||||||||
|
|
||||||||||
| // Read tools/list response | ||||||||||
| let toolsBuffer = ''; | ||||||||||
| while (true) { | ||||||||||
| const { value, done } = await reader.read(); | ||||||||||
| if (done) break; | ||||||||||
|
|
||||||||||
| if (value) { | ||||||||||
| toolsBuffer += new TextDecoder().decode(value); | ||||||||||
| const lines = toolsBuffer.split('\n'); | ||||||||||
|
|
||||||||||
| for (const line of lines) { | ||||||||||
| if (line.trim()) { | ||||||||||
| try { | ||||||||||
| const response = JSON.parse(line.trim()); | ||||||||||
| if (response.id === 2) { | ||||||||||
| if (response.error) { | ||||||||||
| throw new Error(`Tools list failed: ${response.error.message}`); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // Apply filtering and display tools | ||||||||||
| let tools = response.result.tools || []; | ||||||||||
|
|
||||||||||
| if (config.enabledTools) { | ||||||||||
| tools = tools.filter((tool: Tool) => config.enabledTools!.includes(tool.name)); | ||||||||||
| } else if (config.disabledTools) { | ||||||||||
| tools = tools.filter((tool: Tool) => !config.disabledTools!.includes(tool.name)); | ||||||||||
|
||||||||||
| tools = tools.filter((tool: Tool) => !config.disabledTools!.includes(tool.name)); | |
| tools = tools.filter((tool: Tool) => !config.disabledTools.includes(tool.name)); |
Copilot
AI
Aug 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The empty catch block silently ignores all parsing errors. Consider adding a comment explaining why this is intentional or log the error for debugging purposes.
| // Continue reading if this line wasn't valid JSON | |
| } catch (err) { | |
| // Continue reading if this line wasn't valid JSON | |
| process.stderr.write(`Warning: Could not parse line as JSON: ${err instanceof Error ? err.message : String(err)}\n`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using non-null assertion operator (!) is risky here. Consider using optional chaining or a proper null check since the condition already verifies enabledTools exists.