-
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)); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 disabledTools exists.
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||
} | ||||||||||
|
||||||||||
// Print tools in the requested format | ||||||||||
for (const tool of tools) { | ||||||||||
process.stdout.write(`${tool.name}: ${tool.description || 'No description available'}\n`); | ||||||||||
} | ||||||||||
|
||||||||||
return; // Exit successfully | ||||||||||
} | ||||||||||
} catch { | ||||||||||
// Continue reading if this line wasn't valid JSON | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||
continue; | ||||||||||
} | ||||||||||
} | ||||||||||
} | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
throw new Error('No tools/list response received'); | ||||||||||
|
||||||||||
} catch (error) { | ||||||||||
process.stderr.write(`Error listing tools: ${error instanceof Error ? error.message : String(error)}\n`); | ||||||||||
process.exit(1); | ||||||||||
} finally { | ||||||||||
if (targetManager) { | ||||||||||
await targetManager.stopTargetServer(); | ||||||||||
} | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
async function main(): Promise<void> { | ||||||||||
try { | ||||||||||
const config = parseArguments(); | ||||||||||
|
||||||||||
if (config.mode === 'list-tools') { | ||||||||||
await listTools(config); | ||||||||||
return; | ||||||||||
} | ||||||||||
|
||||||||||
const proxyServer = new McpProxyServer(config); | ||||||||||
|
||||||||||
// Handle graceful shutdown | ||||||||||
|
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.
Copilot uses AI. Check for mistakes.