Bläddra i källkod

feat: implement dynamic tool discovery for MCP servers #13

- Created ToolDiscovery class to automatically discover tools from all configured MCP servers
- Updated index.ts to use dynamic tool discovery instead of hardcoded allowedTools
- Removed backward compatibility with hardcoded tool list (as requested)
- Built-in Claude Code tools (Read, Write, Edit, Bash, etc.) are always included
- Tools from custom MCP servers are automatically discovered and formatted as mcp__{server}__{tool}
- Updated CLAUDE.md and client/README.md with comprehensive documentation
- Tool discovery happens at runtime when handling each issue

Benefits:
- No manual configuration needed when adding custom MCP servers
- Repository-specific tool sets based on MCP configuration
- Always up-to-date with actual server capabilities
- Seamless integration with per-repository MCP configuration system

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Claude 9 månader sedan
förälder
incheckning
e3563d187c
4 ändrade filer med 274 tillägg och 62 borttagningar
  1. 30 1
      CLAUDE.md
  2. 60 33
      client/README.md
  3. 14 28
      client/src/index.ts
  4. 170 0
      client/src/toolDiscovery.ts

+ 30 - 1
CLAUDE.md

@@ -299,6 +299,7 @@ npm run build
 - Responds to issue comments automatically
 - Responds to issue comments automatically
 - Environment-specific configuration via `.env`
 - Environment-specific configuration via `.env`
 - **Per-repository MCP configuration support** - Different repositories can use different MCP servers
 - **Per-repository MCP configuration support** - Different repositories can use different MCP servers
+- **Dynamic tool discovery** - Automatically discovers available tools from all configured MCP servers
 
 
 See `client/README.md` for detailed documentation.
 See `client/README.md` for detailed documentation.
 
 
@@ -314,7 +315,8 @@ The client supports flexible MCP server configuration with three priority levels
 - Multiple MCP servers per repository
 - Multiple MCP servers per repository
 - Environment variable substitution using `${VAR_NAME}` syntax
 - Environment variable substitution using `${VAR_NAME}` syntax
 - Secure storage with restricted file permissions (600)
 - Secure storage with restricted file permissions (600)
-- Backward compatible with existing hardcoded configuration
+- **Automatic tool discovery** - Tools from custom MCP servers are automatically available to Claude
+- No need to manually configure allowed tools
 
 
 **Configuration Structure**:
 **Configuration Structure**:
 ```json
 ```json
@@ -342,6 +344,33 @@ The client supports flexible MCP server configuration with three priority levels
 
 
 See `examples/MCP-CONFIG.md` for detailed configuration guide and examples.
 See `examples/MCP-CONFIG.md` for detailed configuration guide and examples.
 
 
+### Dynamic Tool Discovery
+
+The client automatically discovers available tools from all configured MCP servers at runtime:
+
+**How It Works**:
+1. When the client starts, it loads the MCP configuration for the repository
+2. The `ToolDiscovery` class connects to each configured MCP server
+3. It queries each server using the MCP `tools/list` method
+4. Tool names are formatted as `mcp__{server-name}__{tool-name}`
+5. Built-in Claude Code tools (Read, Write, Edit, Bash, etc.) are added automatically
+6. The complete list is passed to the Claude Agent SDK as `allowedTools`
+
+**Benefits**:
+- **No manual configuration** - Add a custom MCP server and its tools are immediately available
+- **Repository-specific tools** - Different repos can have different tool sets based on their MCP config
+- **Always up-to-date** - Tool list reflects the actual capabilities of configured servers
+
+**Built-in Tools** (always available):
+- `Read`, `Write`, `Edit` - File operations
+- `Bash`, `Grep`, `Glob` - Shell operations
+- `TodoWrite`, `Task` - Task management
+- `WebFetch`, `WebSearch` - Web access
+- `NotebookEdit`, `BashOutput`, `KillShell` - Advanced operations
+- `Skill`, `SlashCommand` - Extension points
+
+**Example**: If you configure a custom MCP server named "github" with tools "create_pr" and "list_repos", they will automatically be available as `mcp__github__create_pr` and `mcp__github__list_repos`.
+
 ## Code Modification Guidelines
 ## Code Modification Guidelines
 
 
 **Main Server (src/)**:
 **Main Server (src/)**:

+ 60 - 33
client/README.md

@@ -13,19 +13,23 @@ This client uses the [@anthropic-ai/claude-agent-sdk](https://www.npmjs.com/pack
 - **Issue Automation**: Automatically handles issues assigned to Claude
 - **Issue Automation**: Automatically handles issues assigned to Claude
 - **Comment Processing**: Responds to issue comments and updates
 - **Comment Processing**: Responds to issue comments and updates
 - **Environment-Specific Configuration**: Supports different gogs-mcp paths via environment variables
 - **Environment-Specific Configuration**: Supports different gogs-mcp paths via environment variables
+- **Per-Repository MCP Configuration**: Different repositories can use different MCP servers
+- **Dynamic Tool Discovery**: Automatically discovers and enables tools from all configured MCP servers
 
 
 ## Architecture
 ## Architecture
 
 
 ```
 ```
 client/
 client/
 ├── src/
 ├── src/
-│   ├── claude-client.ts    # Main Claude Agent SDK client
-│   ├── config.ts            # Configuration management
-│   ├── types.ts             # TypeScript type definitions
-│   └── index.ts             # CLI entry point
-├── dist/                    # Compiled JavaScript (generated)
-├── package.json             # Dependencies and scripts
-└── tsconfig.json            # TypeScript configuration
+│   ├── claude-client.ts       # Main Claude Agent SDK client
+│   ├── config.ts              # Configuration management
+│   ├── mcpConfigManager.ts    # MCP configuration loader
+│   ├── toolDiscovery.ts       # Dynamic tool discovery
+│   ├── types.ts               # TypeScript type definitions
+│   └── index.ts               # CLI entry point
+├── dist/                      # Compiled JavaScript (generated)
+├── package.json               # Dependencies and scripts
+└── tsconfig.json              # TypeScript configuration
 ```
 ```
 
 
 ## Installation
 ## Installation
@@ -48,15 +52,26 @@ This compiles TypeScript to JavaScript in the `dist/` directory.
 ### Programmatic API
 ### Programmatic API
 
 
 ```typescript
 ```typescript
-import { ClaudeClient } from './client/dist/index.js';
+import { ClaudeClient, Config, ToolDiscovery } from './client/dist/index.js';
 
 
+// Load configuration
+Config.load('/path/to/repo');
+
+// Generate MCP config (with dynamic per-repo support)
+const mcpConfig = Config.generateMcpConfig('/path/to/repo', 'owner', 'repo');
+
+// Discover available tools automatically
+const allowedTools = await ToolDiscovery.discoverTools(mcpConfig);
+
+// Create client
 const client = new ClaudeClient({
 const client = new ClaudeClient({
   workingDirectory: '/path/to/repo',
   workingDirectory: '/path/to/repo',
   mcpConfigPath: '/path/to/repo/.mcp-gogs.json',
   mcpConfigPath: '/path/to/repo/.mcp-gogs.json',
-  allowedTools: ['mcp__gogs__get_issue', 'mcp__gogs__create_issue_comment', 'Read', 'Write'],
+  allowedTools,  // Automatically discovered tools
   dangerouslySkipPermissions: true
   dangerouslySkipPermissions: true
 });
 });
 
 
+// Handle issue
 const result = await client.handleIssue({
 const result = await client.handleIssue({
   owner: 'username',
   owner: 'username',
   repo: 'repo-name',
   repo: 'repo-name',
@@ -89,39 +104,51 @@ The client reads configuration from the repository's `.env` file:
 GOGS_MCP_PATH=/data/gogs-mcp/dist/index.js
 GOGS_MCP_PATH=/data/gogs-mcp/dist/index.js
 ```
 ```
 
 
-## MCP Tools
+## Dynamic Tool Discovery
+
+The client **automatically discovers** available tools from all configured MCP servers at runtime. You don't need to manually configure the tool list.
+
+### How It Works
+
+1. The client loads the MCP configuration for the repository
+2. `ToolDiscovery` connects to each configured MCP server
+3. It queries each server for available tools using the MCP protocol
+4. Tools are formatted as `mcp__{server-name}__{tool-name}`
+5. Built-in Claude Code tools are added automatically
+6. The complete list is passed to the Claude Agent SDK
 
 
-The client has access to all gogs MCP tools:
+### Built-in Tools (Always Available)
+
+- **File Operations**: `Read`, `Write`, `Edit`
+- **Shell Operations**: `Bash`, `Grep`, `Glob`
+- **Task Management**: `TodoWrite`, `Task`
+- **Web Access**: `WebFetch`, `WebSearch`
+- **Advanced**: `NotebookEdit`, `BashOutput`, `KillShell`, `Skill`, `SlashCommand`
+
+### Default MCP Tools (from gogs-mcp)
+
+When using the default gogs-mcp server, these tools are automatically discovered:
 
 
 **User Tools:**
 **User Tools:**
-- `mcp__gogs__get_current_user`
-- `mcp__gogs__get_user`
-- `mcp__gogs__search_users`
+- `mcp__gogs__get_current_user`, `mcp__gogs__get_user`, `mcp__gogs__search_users`
 
 
 **Repository Tools:**
 **Repository Tools:**
-- `mcp__gogs__list_user_repositories`
-- `mcp__gogs__search_repositories`
-- `mcp__gogs__get_repository`
-- `mcp__gogs__create_repository`
-- `mcp__gogs__delete_repository`
+- `mcp__gogs__list_user_repositories`, `mcp__gogs__search_repositories`, `mcp__gogs__get_repository`
+- `mcp__gogs__create_repository`, `mcp__gogs__delete_repository`
 
 
 **Content Tools:**
 **Content Tools:**
-- `mcp__gogs__get_contents`
-- `mcp__gogs__get_raw_content`
-- `mcp__gogs__list_branches`
-- `mcp__gogs__get_commits`
+- `mcp__gogs__get_contents`, `mcp__gogs__get_raw_content`
+- `mcp__gogs__list_branches`, `mcp__gogs__get_commits`
 
 
 **Issue Tools:**
 **Issue Tools:**
-- `mcp__gogs__list_issues`
-- `mcp__gogs__get_issue`
-- `mcp__gogs__create_issue`
-- `mcp__gogs__update_issue`
-- `mcp__gogs__list_issue_comments`
-- `mcp__gogs__create_issue_comment`
-- `mcp__gogs__update_issue_comment`
-
-**File Tools:**
-- `Read`, `Write`, `Edit`, `Bash`, `Grep`, `Glob`, `TodoWrite`
+- `mcp__gogs__list_issues`, `mcp__gogs__get_issue`, `mcp__gogs__create_issue`, `mcp__gogs__update_issue`
+- `mcp__gogs__list_issue_comments`, `mcp__gogs__create_issue_comment`, `mcp__gogs__update_issue_comment`
+
+### Custom MCP Servers
+
+When you add a custom MCP server to your repository configuration, its tools are **automatically discovered and available** to Claude. No additional configuration needed!
+
+For example, if you configure a "github" MCP server with tools like "create_pr" and "list_repos", they will automatically be available as `mcp__github__create_pr` and `mcp__github__list_repos`.
 
 
 ## Development
 ## Development
 
 

+ 14 - 28
client/src/index.ts

@@ -5,6 +5,7 @@
 export { ClaudeClient } from './claude-client.js';
 export { ClaudeClient } from './claude-client.js';
 export { Config } from './config.js';
 export { Config } from './config.js';
 export { McpConfigManager } from './mcpConfigManager.js';
 export { McpConfigManager } from './mcpConfigManager.js';
+export { ToolDiscovery } from './toolDiscovery.js';
 export type {
 export type {
   IssueData,
   IssueData,
   ClaudeClientConfig,
   ClaudeClientConfig,
@@ -15,6 +16,8 @@ export type {
 // CLI entry point
 // CLI entry point
 if (import.meta.url === `file://${process.argv[1]}`) {
 if (import.meta.url === `file://${process.argv[1]}`) {
   const { ClaudeClient } = await import('./claude-client.js');
   const { ClaudeClient } = await import('./claude-client.js');
+  const { Config } = await import('./config.js');
+  const { ToolDiscovery } = await import('./toolDiscovery.js');
 
 
   // Parse command line arguments
   // Parse command line arguments
   const args = process.argv.slice(2);
   const args = process.argv.slice(2);
@@ -42,38 +45,21 @@ if (import.meta.url === `file://${process.argv[1]}`) {
   const home = process.env.HOME || '/root';
   const home = process.env.HOME || '/root';
   const repoPath = `${home}/${repo}`;
   const repoPath = `${home}/${repo}`;
 
 
+  // Load configuration
+  Config.load(repoPath);
+
+  // Generate MCP configuration for this repository
+  const mcpConfig = Config.generateMcpConfig(repoPath, owner, repo);
+
+  // Discover available tools dynamically
+  console.log('Discovering available tools from MCP servers...');
+  const allowedTools = await ToolDiscovery.discoverTools(mcpConfig);
+
   // Configure Claude client
   // Configure Claude client
   const client = new ClaudeClient({
   const client = new ClaudeClient({
     workingDirectory: repoPath,
     workingDirectory: repoPath,
     mcpConfigPath: `${repoPath}/.mcp-gogs.json`,
     mcpConfigPath: `${repoPath}/.mcp-gogs.json`,
-    allowedTools: [
-      'mcp__gogs__get_current_user',
-      'mcp__gogs__get_user',
-      'mcp__gogs__search_users',
-      'mcp__gogs__list_user_repositories',
-      'mcp__gogs__search_repositories',
-      'mcp__gogs__get_repository',
-      'mcp__gogs__create_repository',
-      'mcp__gogs__delete_repository',
-      'mcp__gogs__get_contents',
-      'mcp__gogs__get_raw_content',
-      'mcp__gogs__list_branches',
-      'mcp__gogs__get_commits',
-      'mcp__gogs__list_issues',
-      'mcp__gogs__get_issue',
-      'mcp__gogs__create_issue',
-      'mcp__gogs__update_issue',
-      'mcp__gogs__list_issue_comments',
-      'mcp__gogs__create_issue_comment',
-      'mcp__gogs__update_issue_comment',
-      'Read',
-      'Write',
-      'Edit',
-      'Bash',
-      'Grep',
-      'Glob',
-      'TodoWrite'
-    ],
+    allowedTools,
     dangerouslySkipPermissions: true
     dangerouslySkipPermissions: true
   });
   });
 
 

+ 170 - 0
client/src/toolDiscovery.ts

@@ -0,0 +1,170 @@
+/**
+ * Tool Discovery for MCP Servers
+ *
+ * Dynamically discovers available tools from all configured MCP servers
+ * and generates the allowedTools array for Claude Agent SDK.
+ */
+
+import { Client } from '@modelcontextprotocol/sdk/client/index.js';
+import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
+import { spawn, ChildProcess } from 'child_process';
+import { MCPServerConfig } from './types.js';
+
+/**
+ * Built-in Claude Code tools that should always be available
+ */
+const BUILT_IN_TOOLS = [
+  'Read',
+  'Write',
+  'Edit',
+  'Bash',
+  'Grep',
+  'Glob',
+  'TodoWrite',
+  'Task',
+  'WebFetch',
+  'WebSearch',
+  'Skill',
+  'SlashCommand',
+  'NotebookEdit',
+  'BashOutput',
+  'KillShell'
+];
+
+export class ToolDiscovery {
+  /**
+   * Discover all available tools from MCP servers
+   */
+  static async discoverTools(mcpConfig: MCPServerConfig): Promise<string[]> {
+    const discoveredTools: Set<string> = new Set();
+
+    // Add built-in tools
+    BUILT_IN_TOOLS.forEach(tool => discoveredTools.add(tool));
+
+    // Discover tools from each MCP server
+    const serverNames = Object.keys(mcpConfig.mcpServers);
+
+    console.log(`Discovering tools from ${serverNames.length} MCP server(s)...`);
+
+    for (const serverName of serverNames) {
+      const serverConfig = mcpConfig.mcpServers[serverName];
+
+      if (serverConfig.type !== 'stdio') {
+        console.warn(`Warning: MCP server '${serverName}' uses type '${serverConfig.type}' - only 'stdio' type is supported for tool discovery. Skipping.`);
+        continue;
+      }
+
+      try {
+        const tools = await this.discoverServerTools(serverName, serverConfig);
+
+        console.log(`  - ${serverName}: discovered ${tools.length} tool(s)`);
+
+        tools.forEach(tool => discoveredTools.add(tool));
+      } catch (error) {
+        console.error(`Error discovering tools from MCP server '${serverName}':`, error instanceof Error ? error.message : String(error));
+        // Continue with other servers even if one fails
+      }
+    }
+
+    const toolsArray = Array.from(discoveredTools).sort();
+    console.log(`Total tools available: ${toolsArray.length}`);
+
+    return toolsArray;
+  }
+
+  /**
+   * Discover tools from a single MCP server
+   */
+  private static async discoverServerTools(
+    serverName: string,
+    serverConfig: { command?: string; args?: string[]; env?: Record<string, string> }
+  ): Promise<string[]> {
+    if (!serverConfig.command) {
+      throw new Error(`MCP server '${serverName}' is missing 'command' field`);
+    }
+
+    let childProcess: ChildProcess | null = null;
+    let client: Client | null = null;
+
+    try {
+      // Prepare environment variables (filter out undefined values)
+      const envVars: Record<string, string> = {};
+
+      // Merge environment variables, filtering out undefined values
+      Object.keys(process.env).forEach(key => {
+        const value = process.env[key];
+        if (value !== undefined) {
+          envVars[key] = value;
+        }
+      });
+
+      // Override with server-specific env vars
+      if (serverConfig.env) {
+        Object.keys(serverConfig.env).forEach(key => {
+          envVars[key] = serverConfig.env![key];
+        });
+      }
+
+      // Spawn the MCP server process
+      childProcess = spawn(
+        serverConfig.command,
+        serverConfig.args || [],
+        {
+          env: envVars,
+          stdio: ['pipe', 'pipe', 'inherit']
+        }
+      );
+
+      if (!childProcess.stdin || !childProcess.stdout) {
+        throw new Error(`Failed to create stdin/stdout pipes for MCP server '${serverName}'`);
+      }
+
+      // Create transport
+      const transport = new StdioClientTransport({
+        command: serverConfig.command,
+        args: serverConfig.args || [],
+        env: envVars
+      });
+
+      // Create MCP client
+      client = new Client({
+        name: `tool-discovery-${serverName}`,
+        version: '1.0.0'
+      }, {
+        capabilities: {}
+      });
+
+      // Connect to the server
+      await client.connect(transport);
+
+      // List available tools
+      const toolsResponse = await client.listTools();
+
+      // Format tool names with mcp__ prefix
+      const tools = toolsResponse.tools.map(tool =>
+        `mcp__${serverName}__${tool.name}`
+      );
+
+      // Close the connection
+      await client.close();
+
+      return tools;
+
+    } catch (error) {
+      // Ensure cleanup
+      if (client) {
+        try {
+          await client.close();
+        } catch (closeError) {
+          // Ignore close errors
+        }
+      }
+
+      if (childProcess) {
+        childProcess.kill();
+      }
+
+      throw error;
+    }
+  }
+}