Skip to content

Commit 938dbfe

Browse files
authored
Merge pull request #1 from context-hub/issue/267
Decouple MCP config generator from renderer and add SSE support
2 parents da2d14c + 6d9f7ec commit 938dbfe

17 files changed

+762
-286
lines changed

context.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
$schema: 'https://raw.githubusercontent.com/context-hub/generator/refs/heads/main/json-schema.json'
2+
3+
documents:
4+
- description: 'PHP Project Structure'
5+
outputPath: docs/php-structure.md
6+
sources:
7+
- type: tree
8+
sourcePaths:
9+
- src
10+
11+
- description: MCP Config generator
12+
outputPath: core/config-generator.md
13+
sources:
14+
- type: file
15+
sourcePaths:
16+
- src/McpConfig
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Butschster\ContextGenerator\McpServer\McpConfig\Client;
6+
7+
use Butschster\ContextGenerator\McpServer\McpConfig\Model\McpConfig;
8+
use Butschster\ContextGenerator\McpServer\McpConfig\Model\OsInfo;
9+
use Symfony\Component\Console\Style\SymfonyStyle;
10+
11+
final class ClaudeDesktopClientStrategy implements ClientStrategyInterface
12+
{
13+
public function getKey(): string
14+
{
15+
return 'claude';
16+
}
17+
18+
public function getLabel(): string
19+
{
20+
return 'Claude Desktop';
21+
}
22+
23+
public function getGeneratorClientKey(): string
24+
{
25+
return 'claude';
26+
}
27+
28+
public function renderConfiguration(
29+
McpConfig $config,
30+
OsInfo $osInfo,
31+
array $options,
32+
SymfonyStyle $output,
33+
): void {
34+
$output->section('Generated Configuration');
35+
36+
$configType = ($options['use_project_path'] ?? false) ? 'Project-specific' : 'Global project registry';
37+
$transportMode = ($options['use_sse'] ?? false) ? 'SSE (Server-Sent Events)' : 'STDIO';
38+
39+
$output->text([
40+
"Configuration type: <info>{$config->clientType}</info>",
41+
"Operating system: <info>{$osInfo->getDisplayName()}</info>",
42+
"Project mode: <info>{$configType}</info>",
43+
"Transport mode: <info>{$transportMode}</info>",
44+
]);
45+
46+
if ($options['use_sse'] ?? false) {
47+
$output->text([
48+
"SSE Host: <info>{$options['sse_host']}</info>",
49+
"SSE Port: <info>{$options['sse_port']}</info>",
50+
]);
51+
}
52+
53+
$output->newLine();
54+
$output->text('Add this to your Claude Desktop configuration file:');
55+
$output->newLine();
56+
$output->writeln('<comment>' . $config->toJson() . '</comment>');
57+
$output->newLine();
58+
59+
$this->showConfigLocation($output, $osInfo);
60+
}
61+
62+
public function renderInstructions(
63+
McpConfig $config,
64+
OsInfo $osInfo,
65+
array $options,
66+
SymfonyStyle $output,
67+
): void {
68+
$output->section('Setup Instructions');
69+
70+
$steps = [
71+
'1. Open Claude Desktop settings: Click your name/initials → Settings → Developer',
72+
'2. Click "Edit Config" to open claude_desktop_config.json',
73+
'3. Add the configuration shown above to the file',
74+
'4. Save the file and completely restart Claude Desktop',
75+
'5. Look for the MCP tools indicator (hammer icon) in the chat input area',
76+
];
77+
78+
foreach ($steps as $step) {
79+
$output->text(' ' . $step);
80+
}
81+
82+
$output->newLine();
83+
84+
// Add mode-specific notes
85+
if ($options['use_project_path'] ?? false) {
86+
$output->note(\implode("\n", [
87+
'Project-specific mode: CTX will only access the specified project path.',
88+
'Good for single-project workflows with focused context.',
89+
]));
90+
} else {
91+
$output->note(\implode("\n", [
92+
'Global registry mode: Use "ctx project:add" to register projects.',
93+
'Switch projects dynamically without editing configuration.',
94+
'Docs: https://docs.ctx.github.io/mcp/projects',
95+
]));
96+
}
97+
98+
// Add SSE-specific notes
99+
if ($options['use_sse'] ?? false) {
100+
$output->note(\implode("\n", [
101+
'SSE mode enabled: Server runs as HTTP endpoint for remote access.',
102+
"Access URL: http://{$options['sse_host']}:{$options['sse_port']}",
103+
'Useful for distributed teams or remote MCP client connections.',
104+
'Ensure firewall allows connections on the specified port.',
105+
]));
106+
}
107+
108+
// Add WSL-specific guidance
109+
if ($osInfo->isWsl()) {
110+
$output->warning(\implode("\n", [
111+
'WSL detected: Ensure ctx is installed in your WSL environment.',
112+
'Use WSL paths (e.g., /home/user/project), not Windows paths.',
113+
'Environment variables are embedded in the bash command.',
114+
]));
115+
}
116+
117+
// Troubleshooting section
118+
$output->section('Troubleshooting');
119+
$tips = [
120+
'No hammer icon? Check claude_desktop_config.json for syntax errors',
121+
'Server not starting? Verify ctx is installed: run "ctx --version"',
122+
'WSL users: Test manually with "bash.exe -c \'ctx server\'"',
123+
'Check logs: Settings → Developer → View Logs',
124+
'',
125+
'Debug CTX issues:',
126+
' • Run with verbose logging: ctx server -vvv',
127+
' • Check log file: ctx-<timestamp>.log in your project directory',
128+
' • Logs show detailed startup, configuration, and error information',
129+
];
130+
131+
if ($options['use_sse'] ?? false) {
132+
$tips[] = '';
133+
$tips[] = 'SSE mode troubleshooting:';
134+
$tips[] = ' • Connection refused? Check if port is already in use';
135+
$tips[] = ' • Can\'t connect remotely? Verify firewall settings';
136+
$tips[] = " • Test endpoint: curl http://{$options['sse_host']}:{$options['sse_port']}";
137+
}
138+
139+
foreach ($tips as $tip) {
140+
if ($tip === '') {
141+
$output->newLine();
142+
} else {
143+
$output->text('' . $tip);
144+
}
145+
}
146+
147+
$output->newLine();
148+
}
149+
150+
private function showConfigLocation(SymfonyStyle $output, OsInfo $osInfo): void
151+
{
152+
$output->text('Configuration file location:');
153+
154+
$paths = match (true) {
155+
$osInfo->isWindows() || $osInfo->isWsl() => [
156+
'%APPDATA%\Claude\claude_desktop_config.json',
157+
'C:\Users\<username>\AppData\Roaming\Claude\claude_desktop_config.json',
158+
],
159+
$osInfo->isMacOs() => [
160+
'~/Library/Application Support/Claude/claude_desktop_config.json',
161+
],
162+
default => [
163+
'~/.config/Claude/claude_desktop_config.json',
164+
'$XDG_CONFIG_HOME/Claude/claude_desktop_config.json',
165+
],
166+
};
167+
168+
foreach ($paths as $path) {
169+
$output->text(" • <info>{$path}</info>");
170+
}
171+
172+
$output->newLine();
173+
}
174+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Butschster\ContextGenerator\McpServer\McpConfig\Client;
6+
7+
use Butschster\ContextGenerator\McpServer\McpConfig\Model\McpConfig;
8+
use Butschster\ContextGenerator\McpServer\McpConfig\Model\OsInfo;
9+
use Symfony\Component\Console\Style\SymfonyStyle;
10+
11+
interface ClientStrategyInterface
12+
{
13+
public function getKey(): string;
14+
15+
public function getLabel(): string;
16+
17+
/**
18+
* Returns the client key supported by the underlying generator (e.g. "claude" or "generic").
19+
*/
20+
public function getGeneratorClientKey(): string;
21+
22+
public function renderConfiguration(
23+
McpConfig $config,
24+
OsInfo $osInfo,
25+
array $options,
26+
SymfonyStyle $output,
27+
): void;
28+
29+
public function renderInstructions(
30+
McpConfig $config,
31+
OsInfo $osInfo,
32+
array $options,
33+
SymfonyStyle $output,
34+
): void;
35+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Butschster\ContextGenerator\McpServer\McpConfig\Client;
6+
7+
final class ClientStrategyRegistry
8+
{
9+
/**
10+
* @var array<string, ClientStrategyInterface>
11+
*/
12+
private array $strategies = [];
13+
14+
/**
15+
* @param array<ClientStrategyInterface> $strategies
16+
*/
17+
public function __construct(
18+
array $strategies = [],
19+
) {
20+
foreach ($strategies as $strategy) {
21+
$this->register($strategy);
22+
}
23+
24+
$this->register(new ClaudeDesktopClientStrategy());
25+
$this->register(new CodexClientStrategy());
26+
$this->register(new CursorClientStrategy());
27+
$this->register(new GenericClientStrategy());
28+
}
29+
30+
public function register(ClientStrategyInterface $strategy): void
31+
{
32+
$this->strategies[$strategy->getKey()] = $strategy;
33+
}
34+
35+
public function getByKey(string $key): ?ClientStrategyInterface
36+
{
37+
$key = \strtolower($key);
38+
return $this->strategies[$key] ?? null;
39+
}
40+
41+
public function getByLabel(string $label): ?ClientStrategyInterface
42+
{
43+
foreach ($this->strategies as $strategy) {
44+
if ($strategy->getLabel() === $label) {
45+
return $strategy;
46+
}
47+
}
48+
return null;
49+
}
50+
51+
/**
52+
* @return string[] Human-friendly labels for interactive choice
53+
*/
54+
public function getChoiceLabels(): array
55+
{
56+
return \array_map(static fn(ClientStrategyInterface $s) => $s->getLabel(), $this->strategies);
57+
}
58+
59+
public function getDefault(): ClientStrategyInterface
60+
{
61+
return $this->strategies['claude'];
62+
}
63+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Butschster\ContextGenerator\McpServer\McpConfig\Client;
6+
7+
use Butschster\ContextGenerator\McpServer\McpConfig\Model\McpConfig;
8+
use Butschster\ContextGenerator\McpServer\McpConfig\Model\OsInfo;
9+
use Symfony\Component\Console\Style\SymfonyStyle;
10+
11+
final class CodexClientStrategy implements ClientStrategyInterface
12+
{
13+
public function getKey(): string
14+
{
15+
return 'codex';
16+
}
17+
18+
public function getLabel(): string
19+
{
20+
return 'Codex';
21+
}
22+
23+
public function getGeneratorClientKey(): string
24+
{
25+
return 'generic';
26+
}
27+
28+
public function renderConfiguration(
29+
McpConfig $config,
30+
OsInfo $osInfo,
31+
array $options,
32+
SymfonyStyle $output,
33+
): void {
34+
$output->section('Generated Configuration');
35+
36+
$output->text('Codex configuration (TOML format):');
37+
$output->newLine();
38+
39+
// Format args for TOML
40+
$args = \array_map(
41+
static fn(string $arg): string => '"' . \str_replace('"', '\\"', $arg) . '"',
42+
$config->args,
43+
);
44+
45+
$toml = "[mcp_servers.ctx]\n"
46+
. "command = \"{$config->command}\"\n"
47+
. 'args = [' . \implode(', ', $args) . "]\n";
48+
49+
$output->writeln('<comment>' . $toml . '</comment>');
50+
$output->newLine();
51+
}
52+
53+
public function renderInstructions(
54+
McpConfig $config,
55+
OsInfo $osInfo,
56+
array $options,
57+
SymfonyStyle $output,
58+
): void {
59+
$output->section('Setup Instructions');
60+
61+
$steps = [
62+
'1. Locate your Codex configuration file',
63+
'2. Copy the TOML snippet above',
64+
'3. Paste it into the [mcp_servers] section',
65+
'4. Ensure the ctx binary is available in your PATH',
66+
'5. Restart Codex to load the new configuration',
67+
];
68+
69+
foreach ($steps as $step) {
70+
$output->text(' ' . $step);
71+
}
72+
73+
$output->newLine();
74+
75+
$output->note(\implode("\n", [
76+
'Codex uses TOML format for MCP server configuration.',
77+
'Make sure ctx is installed and accessible from your terminal.',
78+
]));
79+
80+
$output->newLine();
81+
82+
$output->section('Troubleshooting');
83+
$tips = [
84+
'Server not starting? Verify ctx installation: ctx --version',
85+
'Check Codex logs for connection errors',
86+
'',
87+
'Debug CTX issues:',
88+
' • Enable verbose logging: ctx server -vvv',
89+
' • Check ctx-<timestamp>.log in your project directory',
90+
' • Logs show configuration loading, MCP protocol details, and errors',
91+
];
92+
93+
foreach ($tips as $tip) {
94+
if ($tip === '') {
95+
$output->newLine();
96+
} else {
97+
$output->text('' . $tip);
98+
}
99+
}
100+
101+
$output->newLine();
102+
}
103+
}

0 commit comments

Comments
 (0)