From 280292d72e7703541cd088723aa697dc5d2dbc0d Mon Sep 17 00:00:00 2001 From: Julian lechner Date: Wed, 30 Sep 2026 11:32:27 +0200 Subject: [PATCH] fix(plugins): detect Context7 managed by MCPorter --- scripts/lib/plugins.ps1 | 22 ++++++++++++++- scripts/lib/plugins.sh | 37 +++++++++++++++++++++++-- scripts/tests/test-plugin-selection.ps1 | 17 +++++++++++- scripts/tests/test-plugin-selection.sh | 16 +++++++++-- 4 files changed, 85 insertions(+), 7 deletions(-) diff --git a/scripts/lib/plugins.ps1 b/scripts/lib/plugins.ps1 index b149211..1bfffdd 100644 --- a/scripts/lib/plugins.ps1 +++ b/scripts/lib/plugins.ps1 @@ -65,6 +65,24 @@ function Get-InstalledPlugins { } } +function Test-InstalledMCPServer { + param([string]$HomePath, [string]$Name, [string]$Url) + $path = Join-Path $HomePath '.mcporter/mcporter.json' + foreach ($candidate in @((Split-Path $path), $path)) { + if ((Test-Path -LiteralPath $candidate) -and ((Get-Item -LiteralPath $candidate -Force).Attributes -band [System.IO.FileAttributes]::ReparsePoint)) { throw 'MCPorter configuration path contains a link.' } + } + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { return $false } + $config = Get-Content -LiteralPath $path -Raw | ConvertFrom-Json + if ($config -isnot [pscustomobject]) { throw 'Invalid MCPorter server configuration.' } + $servers = $config.PSObject.Properties['mcpServers'] + if ($null -eq $servers) { return $false } + if ($servers.Value -isnot [pscustomobject]) { throw 'Invalid MCPorter server configuration.' } + $server = $servers.Value.PSObject.Properties[$Name] + if ($null -eq $server) { return $false } + if ($server.Value -isnot [pscustomobject]) { throw 'Invalid MCPorter server entry.' } + return $server.Value.baseUrl -eq $Url +} + function Read-PluginToggleSelection { param( [Parameter(Mandatory=$true)][string[]]$Names, @@ -102,7 +120,9 @@ function Select-ConfiguredPlugins { if ($Mode -eq 'Install') { $true; return } if ($_.codex_method -eq 'cli') { return $null -ne (Get-Command $_.codex_source -ErrorAction SilentlyContinue) } $entry = $_ - $found = $false + $found = if ($entry.mcporter_name -and $entry.mcporter_url) { + Test-InstalledMCPServer -HomePath $HomePath -Name $entry.mcporter_name -Url $entry.mcporter_url + } else { $false } foreach ($referenceClient in $referenceClients) { $selector = if ($referenceClient -eq 'Claude') { $entry.claude_plugin } else { $entry.codex_plugin } if ($referenceClient -eq 'Codex' -and $entry.codex_method -eq 'qmd') { diff --git a/scripts/lib/plugins.sh b/scripts/lib/plugins.sh index d0e26fc..813eebc 100755 --- a/scripts/lib/plugins.sh +++ b/scripts/lib/plugins.sh @@ -56,9 +56,9 @@ get_plugin_names() { plugin_field() { local root=$1 target=$2 field=$3 # shellcheck disable=SC2034 - local name claude_marketplace claude_plugin codex_marketplace codex_plugin codex_method codex_source codex_skill + local name claude_marketplace claude_plugin codex_marketplace codex_plugin codex_method codex_source codex_skill mcporter_name mcporter_url # shellcheck disable=SC2034 - while IFS=$'\t' read -r name claude_marketplace claude_plugin codex_marketplace codex_plugin codex_method codex_source codex_skill; do + while IFS=$'\t' read -r name claude_marketplace claude_plugin codex_marketplace codex_plugin codex_method codex_source codex_skill mcporter_name mcporter_url; do [ "$name" = "$target" ] || continue printf '%s\n' "${!field}" return 0 @@ -66,6 +66,30 @@ plugin_field() { return 1 } +mcporter_server_installed() { + local path="$1/.mcporter/mcporter.json" server=$2 url=$3 + if [ -L "$1/.mcporter" ] || [ -L "$path" ]; then printf 'MCPorter configuration path contains a link.\n' >&2; return 2; fi + [ -f "$path" ] || return 1 + python3 -c ' +import json, sys +try: + with open(sys.argv[1], encoding="utf-8-sig") as stream: + config = json.load(stream) + if not isinstance(config, dict): + raise ValueError("Invalid MCPorter server configuration") + servers = config.get("mcpServers", {}) + if not isinstance(servers, dict): + raise ValueError("Invalid MCPorter server configuration") + entry = servers.get(sys.argv[2]) + if entry is not None and not isinstance(entry, dict): + raise ValueError("Invalid MCPorter server entry") + sys.exit(0 if entry is not None and entry.get("baseUrl") == sys.argv[3] else 1) +except (OSError, ValueError, AttributeError) as error: + print("Invalid MCPorter configuration: " + str(error), file=sys.stderr) + sys.exit(2) +' "$path" "$server" "$url" +} + plugin_toggleable() { return 0 } @@ -128,6 +152,13 @@ select_configured_plugins() { continue fi found=false + local mcporter_name mcporter_url status + mcporter_name=$(plugin_field "$root" "$name" mcporter_name) + mcporter_url=$(plugin_field "$root" "$name" mcporter_url) + if [ -n "$mcporter_name" ] && [ "$mcporter_name" != - ] && [ -n "$mcporter_url" ] && [ "$mcporter_url" != - ]; then + if mcporter_server_installed "${home_path:-$HOME}" "$mcporter_name" "$mcporter_url"; then found=true + else status=$?; [ "$status" -eq 1 ] || return "$status"; fi + fi for reference_client in "${reference_clients[@]}"; do if [ "$reference_client" = claude ]; then selector=$(plugin_field "$root" "$name" claude_plugin); else selector=$(plugin_field "$root" "$name" codex_plugin); fi if [ "$reference_client" = codex ] && [ "$(plugin_field "$root" "$name" codex_method)" = qmd ]; then @@ -138,7 +169,7 @@ select_configured_plugins() { { [ -d "$HOME/.agents/skills/$skill_name" ] || [ -d "$HOME/.codex/skills/$skill_name" ]; } && found=true elif plugin_installed "$reference_client" "$selector"; then found=true else - local status=$? + status=$? [ "$status" -eq 1 ] || return "$status" fi done diff --git a/scripts/tests/test-plugin-selection.ps1 b/scripts/tests/test-plugin-selection.ps1 index c69d947..61bab1e 100644 --- a/scripts/tests/test-plugin-selection.ps1 +++ b/scripts/tests/test-plugin-selection.ps1 @@ -37,7 +37,8 @@ catch { $failed = $true } $env:AI_CONFIG_VERBOSE = $null if (-not $failed -or $diagnostics -notcontains 'download progress' -or $diagnostics -notcontains 'WARN test warning') { throw 'Plugin failure must retain full diagnostics and fail.' } $global:LASTEXITCODE = 0 -function codex { $script:observedCodexHome = $env:CODEX_HOME; $script:observedProfile = $env:USERPROFILE; '{"installed":[{"pluginId":"ponytail@ponytail"},{"pluginId":"i-have-adhd@i-have-adhd"},{"pluginId":"superpowers@openai-curated-remote"},{"pluginId":"context7@context7-marketplace"},{"pluginId":"caveman@thinkhome-caveman"}],"available":[]}' } +$script:codexInventory = '{"installed":[{"pluginId":"ponytail@ponytail"},{"pluginId":"i-have-adhd@i-have-adhd"},{"pluginId":"superpowers@openai-curated-remote"},{"pluginId":"context7@context7-marketplace"},{"pluginId":"caveman@thinkhome-caveman"}],"available":[]}' +function codex { $script:observedCodexHome = $env:CODEX_HOME; $script:observedProfile = $env:USERPROFILE; $script:codexInventory } $testHome = Join-Path ([System.IO.Path]::GetTempPath()) 'ai-config-plugin-test-home' $previousCodexHome = [Environment]::GetEnvironmentVariable('CODEX_HOME', 'Process') $previousProfile = [Environment]::GetEnvironmentVariable('USERPROFILE', 'Process') @@ -51,6 +52,20 @@ $script:answers.Enqueue('4') $script:answers.Enqueue('done') $selection = Select-ConfiguredPlugins -RepositoryRoot (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -Client Codex -Mode Update if (@($selection.Deselected | Where-Object { $_.name -eq 'Context7' }).Count -ne 1) { throw 'Only the explicitly unchecked installed plugin must be deselected.' } +$mcporterHome = Join-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) ('.context7-test-' + [Guid]::NewGuid()) +try { + $configPath = Join-Path $mcporterHome '.mcporter/mcporter.json' + New-Item (Split-Path $configPath) -ItemType Directory -Force | Out-Null + [System.IO.File]::WriteAllText($configPath, '{"mcpServers":{"context7":{"baseUrl":"https://mcp.context7.com/mcp"}}}') + $script:codexInventory = '{"installed":[],"available":[]}' + $script:answers.Enqueue('done') + $selection = Select-ConfiguredPlugins -RepositoryRoot $root -Client Codex -Mode Update -HomePath $mcporterHome + if (@($selection.Selected | Where-Object { $_.name -eq 'Context7' }).Count -ne 1) { throw 'MCPorter Context7 must start checked when updating.' } + [System.IO.File]::WriteAllText($configPath, '{"mcpServers":{"context7":{"baseUrl":"https://example.invalid/mcp"}}}') + $script:answers.Enqueue('done') + $selection = Select-ConfiguredPlugins -RepositoryRoot $root -Client Codex -Mode Update -HomePath $mcporterHome + if (@($selection.Deselected | Where-Object { $_.name -eq 'Context7' }).Count -ne 1) { throw 'A different MCPorter server must not select Context7.' } +} finally { Remove-Item -LiteralPath $mcporterHome -Recurse -Force -ErrorAction SilentlyContinue } $script:managedArguments = @() function python { $script:managedArguments = @($args); $global:LASTEXITCODE = 0 } $entry = [pscustomobject]@{ name = 'Test'; codex_method = 'plugin'; codex_marketplace = '-'; codex_plugin = 'test@market' } diff --git a/scripts/tests/test-plugin-selection.sh b/scripts/tests/test-plugin-selection.sh index 1f2b97c..3856c15 100644 --- a/scripts/tests/test-plugin-selection.sh +++ b/scripts/tests/test-plugin-selection.sh @@ -14,8 +14,9 @@ root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) # shellcheck disable=SC2034 names=(A B C) checked=(true false true) -toggle_log=$(mktemp) -trap 'rm -f -- "$toggle_log"' EXIT +toggle_log=$(mktemp "$root/.plugin-toggle.XXXXXX") +test_home=$(mktemp -d "$root/.context7-test.XXXXXX") +trap 'rm -f -- "$toggle_log" "$test_home/.mcporter/mcporter.json"; rmdir -- "$test_home/.mcporter" "$test_home" 2>/dev/null || true' EXIT read_plugin_toggle_selection names checked <<< $'1\n2\ndone' > "$toggle_log" [ "$summary" = true ] || cat "$toggle_log" @@ -36,6 +37,17 @@ if diagnostics=$(summary=true run_plugin_command false test_plugin_output fail 2 exit 1 fi [[ "$diagnostics" == *'download progress'* ]] && [[ "$diagnostics" == *'WARN test warning'* ]] +mkdir -p "$test_home/.mcporter" +printf '%s\n' '{"mcpServers":{"context7":{"baseUrl":"https://mcp.context7.com/mcp"}}}' > "$test_home/.mcporter/mcporter.json" +home_path=$test_home +plugin_installed() { return 1; } +selected=() +deselected=() +select_configured_plugins "$root" codex Update true selected deselected +[[ " ${selected[*]} " == *' Context7 '* ]] || { printf 'MCPorter Context7 must start checked when updating.\n' >&2; exit 1; } +printf '%s\n' '{"mcpServers":{"context7":{"baseUrl":"https://example.invalid/mcp"}}}' > "$test_home/.mcporter/mcporter.json" +select_configured_plugins "$root" codex Update true selected deselected +[[ " ${deselected[*]} " == *' Context7 '* ]] || { printf 'A different MCPorter server must not select Context7.\n' >&2; exit 1; } # shellcheck disable=SC2034 selected=() plugin_installed() { printf 'Empty selection must not query installed plugins.\n' >&2; exit 1; }