Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 57 additions & 57 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,11 @@ what contexts might be useful.
ctx generate
```

3. Use with your favorite AI:
- Copy the generated markdown files to your AI chat
- Or use the built-in MCP server with Claude Desktop
- Or process locally with open-source models
3. Use with your favorite AI:

- Copy the generated markdown files to your AI chat
- Or use the built-in MCP server with your MCP client (e.g., Claude Desktop, Cursor, Continue, Windsurf)
- Or process locally with open-source models

## Real-World Use Cases

Expand Down Expand Up @@ -220,58 +220,58 @@ Configuration → Sources → Filters → Modifiers → Output
- **Modifiers**: How to transform content (extract signatures, remove comments)
- **Output**: Structured markdown ready for AI consumption

## Connect to Claude Desktop (Optional)
For a more seamless experience, you can connect Context Generator directly to Claude AI using the MCP server.
```bash
# Auto-detect OS and generate configuration
ctx mcp:config
```
This command:
- 🔍 **Auto-detects your OS** (Windows, Linux, macOS, WSL)
- 🎯 **Generates the right config** for your environment
- 📋 **Provides copy-paste ready** JSON for Claude Desktop
- 🧭 **Includes setup instructions** and troubleshooting tips
**Global Registry Mode** (recommended for multiple projects):
```json
{
"mcpServers": {
"ctx": {
"command": "ctx",
"args": [
"server"
]
}
}
}
```
If you prefer manual setup, point the MCP client to the Context Generator server:
```json
{
"mcpServers": {
"ctx": {
"command": "ctx",
"args": [
"server",
"-c",
"/path/to/project"
]
}
}
}
```
> **Note:** Read more about [MCP Server](https://docs.ctxgithub.com/mcp/#setting-up) for detailed setup
> instructions and troubleshooting.
Now you can ask Claude questions about your codebase without manually uploading context files!
## Connect to an MCP Client (Optional)

For a more seamless experience, you can connect CTX to any MCP-compatible client using the built-in MCP server.

```bash
# Interactive setup: detect OS and install config for your client
ctx mcp:config -i
```

This command:

- 🔍 Auto-detects your OS (Windows, Linux, macOS, WSL)
- 🧩 Lets you choose your MCP client (e.g., Claude Desktop, Cursor, Continue, Windsurf)
- 🎯 Generates and optionally installs the correct config for your environment
- 📋 Provides copy‑paste ready JSON if you prefer manual setup
- 🧭 Includes setup instructions and troubleshooting tips

**Global Registry Mode** (recommended for multiple projects/clients):

```json
{
"mcpServers": {
"ctx": {
"command": "ctx",
"args": [
"server"
]
}
}
}
```

If you prefer manual setup, point your MCP client to the CTX server:

```json
{
"mcpServers": {
"ctx": {
"command": "ctx",
"args": [
"server",
"-c",
"/path/to/project"
]
}
}
}
```

> Note: Read more about the [MCP server](https://docs.ctxgithub.com/mcp/#setting-up) for detailed setup instructions and troubleshooting. Specific config file locations vary by client.

Now you can use your preferred MCP client (including Claude Desktop) to ask questions about your codebase without manually uploading context files.

## Custom Tools

Expand Down
42 changes: 28 additions & 14 deletions src/McpServer/Console/McpConfigCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Butschster\ContextGenerator\McpServer\McpConfig\ConfigGeneratorInterface;
use Butschster\ContextGenerator\McpServer\McpConfig\Renderer\McpConfigRenderer;
use Butschster\ContextGenerator\McpServer\McpConfig\Service\OsDetectionService;
use Butschster\ContextGenerator\McpServer\McpConfig\Client\ClientStrategyRegistry;
use Spiral\Console\Attribute\Option;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
Expand Down Expand Up @@ -43,7 +44,7 @@ final class McpConfigCommand extends BaseCommand
#[Option(
name: 'client',
shortcut: 'c',
description: 'MCP client type (claude, generic)',
description: 'MCP client type (claude, codex, cursor, generic)',
)]
protected string $client = 'generic';

Expand Down Expand Up @@ -80,20 +81,22 @@ public function __invoke(
// Determine configuration approach
$options = $this->buildConfigOptions($dirs);

// Generate configuration
// Resolve selected client strategy
$registry = new ClientStrategyRegistry();
$strategy = $registry->getByKey($this->client) ?? $registry->getDefault();

// Generate configuration via vendor generator (supports claude/generic)
$config = $configGenerator->generate(
client: $this->client,
client: $strategy->getGeneratorClientKey(),
osInfo: $osInfo,
projectPath: $options['project_path'] ?? (string) $dirs->getRootPath(),
options: $options,
);

// Render the configuration
$renderer->renderConfiguration($config, $osInfo, $options);

// Show explanations if requested
// Render using strategy
$strategy->renderConfiguration($renderer, $config, $osInfo, $options, $this->output);
if ($this->explain) {
$renderer->renderExplanation($config, $osInfo, $options);
$strategy->renderExplanation($renderer, $config, $osInfo, $options, $this->output);
}

return Command::SUCCESS;
Expand All @@ -108,12 +111,23 @@ private function runInteractiveMode(
$renderer->renderInteractiveWelcome();

// Ask about client type
$clientType = $this->output->choice(
$registry = new ClientStrategyRegistry();

// Build interactive choices. We pass human labels for display, but
// also accept typed keys (e.g. "codex") as valid input.
$choice = $this->output->choice(
'Which MCP client are you configuring?',
['claude' => 'Claude Desktop', 'generic' => 'Generic MCP Client'],
'generic',
$registry->getChoiceLabels(),
$registry->getDefault()->getLabel(),
);

// Resolve strategy by label first, then by key (case-insensitive).
// This fixes a bug where typing a key like "codex" fell back to the
// default (Claude) because we only matched by label.
$strategy = $registry->getByLabel($choice)
?? $registry->getByKey(\strtolower(\trim((string) $choice)))
?? $registry->getDefault();

// Auto-detect OS
$osInfo = $osDetection->detect();
$renderer->renderDetectedEnvironment($osInfo);
Expand Down Expand Up @@ -174,14 +188,14 @@ private function runInteractiveMode(

// Generate and display configuration
$config = $configGenerator->generate(
client: $clientType,
client: $strategy->getGeneratorClientKey(),
osInfo: $osInfo,
projectPath: $projectPath,
options: $options,
);

$renderer->renderConfiguration($config, $osInfo, $options);
$renderer->renderExplanation($config, $osInfo, $options);
$strategy->renderConfiguration($renderer, $config, $osInfo, $options, $this->output);
$strategy->renderExplanation($renderer, $config, $osInfo, $options, $this->output);

return Command::SUCCESS;
}
Expand Down
39 changes: 39 additions & 0 deletions src/McpServer/McpConfig/Client/AbstractClientStrategy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace Butschster\ContextGenerator\McpServer\McpConfig\Client;

use Butschster\ContextGenerator\McpServer\McpConfig\Model\McpConfig;
use Butschster\ContextGenerator\McpServer\McpConfig\Model\OsInfo;
use Butschster\ContextGenerator\McpServer\McpConfig\Renderer\McpConfigRenderer;
use Symfony\Component\Console\Style\SymfonyStyle;

abstract class AbstractClientStrategy implements ClientStrategyInterface
{
public function renderConfiguration(
McpConfigRenderer $renderer,
McpConfig $config,
OsInfo $osInfo,
array $options,
SymfonyStyle $output,
): void {
// Default rendering delegates to shared renderer
$renderer->renderConfiguration($config, $osInfo, $options);
}

public function renderExplanation(
McpConfigRenderer $renderer,
McpConfig $config,
OsInfo $osInfo,
array $options,
SymfonyStyle $output,
): void {
// Default explanation delegates to shared renderer
$renderer->renderExplanation($config, $osInfo, $options);

$this->renderAdditionalNotes($output, $osInfo, $options);
}

protected function renderAdditionalNotes(SymfonyStyle $output, OsInfo $osInfo, array $options): void {}
}
23 changes: 23 additions & 0 deletions src/McpServer/McpConfig/Client/ClaudeDesktopClientStrategy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Butschster\ContextGenerator\McpServer\McpConfig\Client;

final class ClaudeDesktopClientStrategy extends AbstractClientStrategy
{
public function getKey(): string
{
return 'claude';
}

public function getLabel(): string
{
return 'Claude Desktop';
}

public function getGeneratorClientKey(): string
{
return 'claude';
}
}
38 changes: 38 additions & 0 deletions src/McpServer/McpConfig/Client/ClientStrategyInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace Butschster\ContextGenerator\McpServer\McpConfig\Client;

use Butschster\ContextGenerator\McpServer\McpConfig\Model\McpConfig;
use Butschster\ContextGenerator\McpServer\McpConfig\Model\OsInfo;
use Butschster\ContextGenerator\McpServer\McpConfig\Renderer\McpConfigRenderer;
use Symfony\Component\Console\Style\SymfonyStyle;

interface ClientStrategyInterface
{
public function getKey(): string;

public function getLabel(): string;

/**
* Returns the client key supported by the underlying generator (e.g. "claude" or "generic").
*/
public function getGeneratorClientKey(): string;

public function renderConfiguration(
McpConfigRenderer $renderer,
McpConfig $config,
OsInfo $osInfo,
array $options,
SymfonyStyle $output,
): void;

public function renderExplanation(
McpConfigRenderer $renderer,
McpConfig $config,
OsInfo $osInfo,
array $options,
SymfonyStyle $output,
): void;
}
55 changes: 55 additions & 0 deletions src/McpServer/McpConfig/Client/ClientStrategyRegistry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace Butschster\ContextGenerator\McpServer\McpConfig\Client;

final class ClientStrategyRegistry
{
/** @var array<string, ClientStrategyInterface> */
private array $strategies;

public function __construct()
{
$this->strategies = [];

$this->register(new ClaudeDesktopClientStrategy());
$this->register(new CodexClientStrategy());
$this->register(new CursorClientStrategy());
$this->register(new GenericClientStrategy());
}

public function register(ClientStrategyInterface $strategy): void
{
$this->strategies[$strategy->getKey()] = $strategy;
}

public function getByKey(string $key): ?ClientStrategyInterface
{
$key = \strtolower($key);
return $this->strategies[$key] ?? null;
}

public function getByLabel(string $label): ?ClientStrategyInterface
{
foreach ($this->strategies as $strategy) {
if ($strategy->getLabel() === $label) {
return $strategy;
}
}
return null;
}

/**
* @return string[] Human-friendly labels for interactive choice
*/
public function getChoiceLabels(): array
{
return \array_map(static fn(ClientStrategyInterface $s) => $s->getLabel(), $this->strategies);
}

public function getDefault(): ClientStrategyInterface
{
return $this->strategies['claude'];
}
}
Loading
Loading