runlayer 0.22.0__tar.gz → 0.22.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {runlayer-0.22.0 → runlayer-0.22.2}/PKG-INFO +7 -6
- {runlayer-0.22.0 → runlayer-0.22.2}/README.md +5 -5
- {runlayer-0.22.0 → runlayer-0.22.2}/pyproject.toml +2 -1
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/commands/setup.py +1 -1
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/commands/skills.py +58 -23
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/plugins/installer.py +4 -8
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/scan/clients.py +34 -0
- runlayer-0.22.2/runlayer_cli/scan/codex_plugins.py +168 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/scan/config_parser.py +110 -8
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/scan/cursor_plugins.py +2 -21
- runlayer-0.22.2/runlayer_cli/scan/opencode_plugins.py +198 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/scan/plugin_scanner.py +297 -1
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/scan/project_scanner.py +39 -11
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/scan/service.py +48 -4
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/scan/skill_scanner.py +1 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/skills/sync_engine.py +3 -3
- runlayer-0.22.2/tests/test_opencode_plugins.py +287 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_plugin_scanner.py +307 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_scan_clients.py +51 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_scan_parser.py +236 -2
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_scan_skills.py +17 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_skills_commands.py +160 -3
- {runlayer-0.22.0 → runlayer-0.22.2}/.gitignore +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/.python-version +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/AGENTS.md +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/CLAUDE.md +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/LICENSE +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/Makefile +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/development.md +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/hooks/README.md +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/hooks/__init__.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/hooks/runlayer-hook.sh +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/__init__.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/api.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/commands/__init__.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/commands/auth.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/commands/cache.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/commands/deploy.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/commands/hooks.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/commands/logs.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/commands/org_api_key.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/commands/plugins.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/commands/scan.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/config.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/console.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/credential_store.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/deploy/__init__.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/deploy/config.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/deploy/docker_builder.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/deploy/service.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/env_substitution.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/index.html +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/logging.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/main.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/metrics.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/middleware.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/models.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/oauth.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/oauth_callback.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/paths.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/plugins/__init__.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/plugins/claude_manifest.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/plugins/discovery.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/plugins/models.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/plugins/sync_engine.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/scan/__init__.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/scan/claude_code_plugins.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/scan/device.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/scan/file_collector.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/scan/openclaw_detector.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/skill_identifier/AGENTS.md +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/skill_identifier/CLAUDE.md +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/skill_identifier/README.md +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/skill_identifier/__init__.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/skill_identifier/hashing.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/skill_identifier/merkle.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/skill_identifier/skill_id.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/skill_identifier/types.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/skills/__init__.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/skills/discovery.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/skills/installer.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/skills/models.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/symbols.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/sync.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/verified_local_proxy/README.md +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/verified_local_proxy/__init__.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/verified_local_proxy/config.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/verified_local_proxy/exceptions.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/verified_local_proxy/proxy.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/verified_local_proxy/verification/__init__.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/verified_local_proxy/verification/base.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/verified_local_proxy/verification/macos.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/runlayer_cli/verified_local_proxy/verification/windows.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/__init__.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/cache/__init__.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/cache/test_cache.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/conftest.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/deploy/__init__.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/deploy/test_lifecycle.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/deploy/test_validate.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/client_specs/claude_code.md +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/client_specs/claude_desktop.md +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/client_specs/codex.md +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/client_specs/cursor.md +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/client_specs/goose.md +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/client_specs/opencode.md +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/client_specs/vscode.md +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/client_specs/windsurf.md +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/client_specs/zed.md +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/.claude-plugin/marketplace.json +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/.claude-plugin/plugin.json +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/.lsp.json +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/.mcp.json +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/README.md +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/agents/README.md +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/agents/code-reviewer.md +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/commands/review.md +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/config.json +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/hooks/hooks.json +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/hooks/validate.sh +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/notes.txt +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/scripts/deploy.sh +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/settings.json +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/skills/code-review/SKILL.md +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/skills/code-review/prompts.md +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/skills/ticket-triage/SKILL.md +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/skills/ticket-triage/helper.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/skills-v2/notes.md +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/skillsets/reference.md +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/fixtures/full_plugin/tool.ts +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/org_api_key/__init__.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/org_api_key/test_org_api_key.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/plugins/__init__.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/plugins/test_add.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/plugins/test_mcp_fallback.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/plugins/test_push.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/run/__init__.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/run/test_run.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/scan/__init__.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/scan/test_scan.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/setup/test_install_opencode.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/skills/__init__.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/skills/test_lifecycle.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/test_help_matrix.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/version/__init__.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/e2e/version/test_version.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/fixtures/claude_code_config.json +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/fixtures/claude_desktop_config.json +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/fixtures/cursor_config.json +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/fixtures/empty_config.json +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/fixtures/goose_config.yaml +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/fixtures/invalid_json.txt +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/fixtures/sse_server_config.json +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/fixtures/vscode_config.json +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/skill_identifier/__init__.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/skill_identifier/test_merkle.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/skill_identifier/test_skill_id.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_api.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_auth.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_backwards_compatibility.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_cache.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_claude_code_plugins.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_claude_json_integration.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_cli.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_cli_backwards_compat.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_config.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_credential_store.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_cursor_plugins.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_deploy_service.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_docker_builder.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_env_substitution.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_hook_script.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_hooks_relay.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_logging.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_logs.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_metrics.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_middleware.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_oauth_browser_lock.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_oauth_token_storage.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_org_api_key_commands.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_plugins_commands.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_plugins_discovery.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_plugins_installer.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_plugins_sync.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_scan_device.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_scan_openclaw.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_scan_project.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_scan_service.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_setup_hooks.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_setup_install_claude_desktop.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_setup_install_config_formats.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_setup_install_local_servers.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_skills_discovery.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_skills_installer.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_skills_sync.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/test_symbols.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/verified_local_proxy/__init__.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/verified_local_proxy/test_config.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/verified_local_proxy/test_proxy.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/verified_local_proxy/test_verification/__init__.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/verified_local_proxy/test_verification/test_base.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/verified_local_proxy/test_verification/test_macos.py +0 -0
- {runlayer-0.22.0 → runlayer-0.22.2}/tests/verified_local_proxy/test_verification/test_windows.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: runlayer
|
|
3
|
-
Version: 0.22.
|
|
3
|
+
Version: 0.22.2
|
|
4
4
|
Summary: A command-line interface for running MCP servers via HTTP transport
|
|
5
5
|
Project-URL: Homepage, https://runlayer.com
|
|
6
6
|
Project-URL: Documentation, https://docs.runlayer.com/
|
|
@@ -229,6 +229,7 @@ Requires-Dist: pyyaml>=6.0.0
|
|
|
229
229
|
Requires-Dist: questionary>=2.0.0
|
|
230
230
|
Requires-Dist: rich>=13.0.0
|
|
231
231
|
Requires-Dist: structlog>=25.4.0
|
|
232
|
+
Requires-Dist: tomli>=2.0.0; python_version < '3.11'
|
|
232
233
|
Requires-Dist: typer>=0.17.4
|
|
233
234
|
Description-Content-Type: text/markdown
|
|
234
235
|
|
|
@@ -531,7 +532,7 @@ The `--secret` flag still accepts any raw API key (user or org) directly and tak
|
|
|
531
532
|
|
|
532
533
|
### `scan` - Scan MCP Client Configurations
|
|
533
534
|
|
|
534
|
-
Scan for MCP server configurations across supported clients (Cursor, Claude Desktop, Claude Code, VS Code, Windsurf) and submit results to Runlayer for classification.
|
|
535
|
+
Scan for MCP server configurations, skills, and plugins across supported clients (Cursor, Claude Desktop, Claude Code, VS Code, Windsurf, Goose, Zed, OpenCode, Codex) and submit results to Runlayer for classification.
|
|
535
536
|
|
|
536
537
|
#### Command Options
|
|
537
538
|
|
|
@@ -732,12 +733,12 @@ uvx runlayer skills push [PATH] --namespace <namespace> [OPTIONS]
|
|
|
732
733
|
uvx runlayer skills scan <PATH> [OPTIONS]
|
|
733
734
|
```
|
|
734
735
|
|
|
735
|
-
- `PATH`: Skill dir
|
|
736
|
-
- `--name`: Override skill name sent to the security scan API
|
|
736
|
+
- `PATH`: Skill dir, `SKILL.md` path, or a root containing many skills
|
|
737
|
+
- `--name`: Override skill name sent to the security scan API for single-skill scans only
|
|
737
738
|
- `--fail-on`: Exit non-zero on `warn` or `block`
|
|
738
|
-
- Output is always JSON
|
|
739
|
+
- Output is always JSON. Single-skill scans return one scan object; multi-skill scans return `{ "skills": [...] }`
|
|
739
740
|
|
|
740
|
-
This calls the security scan API to score one local
|
|
741
|
+
This calls the security scan API to score one or many local skills without pushing them to the
|
|
741
742
|
Runlayer skills catalog or AI Watch shadow-skill discovery flow.
|
|
742
743
|
|
|
743
744
|
#### `skills add`
|
|
@@ -297,7 +297,7 @@ The `--secret` flag still accepts any raw API key (user or org) directly and tak
|
|
|
297
297
|
|
|
298
298
|
### `scan` - Scan MCP Client Configurations
|
|
299
299
|
|
|
300
|
-
Scan for MCP server configurations across supported clients (Cursor, Claude Desktop, Claude Code, VS Code, Windsurf) and submit results to Runlayer for classification.
|
|
300
|
+
Scan for MCP server configurations, skills, and plugins across supported clients (Cursor, Claude Desktop, Claude Code, VS Code, Windsurf, Goose, Zed, OpenCode, Codex) and submit results to Runlayer for classification.
|
|
301
301
|
|
|
302
302
|
#### Command Options
|
|
303
303
|
|
|
@@ -498,12 +498,12 @@ uvx runlayer skills push [PATH] --namespace <namespace> [OPTIONS]
|
|
|
498
498
|
uvx runlayer skills scan <PATH> [OPTIONS]
|
|
499
499
|
```
|
|
500
500
|
|
|
501
|
-
- `PATH`: Skill dir
|
|
502
|
-
- `--name`: Override skill name sent to the security scan API
|
|
501
|
+
- `PATH`: Skill dir, `SKILL.md` path, or a root containing many skills
|
|
502
|
+
- `--name`: Override skill name sent to the security scan API for single-skill scans only
|
|
503
503
|
- `--fail-on`: Exit non-zero on `warn` or `block`
|
|
504
|
-
- Output is always JSON
|
|
504
|
+
- Output is always JSON. Single-skill scans return one scan object; multi-skill scans return `{ "skills": [...] }`
|
|
505
505
|
|
|
506
|
-
This calls the security scan API to score one local
|
|
506
|
+
This calls the security scan API to score one or many local skills without pushing them to the
|
|
507
507
|
Runlayer skills catalog or AI Watch shadow-skill discovery flow.
|
|
508
508
|
|
|
509
509
|
#### `skills add`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "runlayer"
|
|
3
|
-
version = "0.22.
|
|
3
|
+
version = "0.22.2"
|
|
4
4
|
description = "A command-line interface for running MCP servers via HTTP transport"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -29,6 +29,7 @@ dependencies = [
|
|
|
29
29
|
"rich>=13.0.0",
|
|
30
30
|
"python-dotenv>=1.0.0",
|
|
31
31
|
"json5>=0.12.1",
|
|
32
|
+
"tomli>=2.0.0; python_version < '3.11'",
|
|
32
33
|
"questionary>=2.0.0",
|
|
33
34
|
"keyring>=25.0.0,<26",
|
|
34
35
|
]
|
|
@@ -1175,7 +1175,7 @@ def _build_opencode_server_entry(spec: InstallServerSpec) -> dict[str, Any]:
|
|
|
1175
1175
|
}
|
|
1176
1176
|
return entry
|
|
1177
1177
|
|
|
1178
|
-
entry = {"enabled": True, "type": "remote", "url": spec.proxy_url}
|
|
1178
|
+
entry: dict[str, Any] = {"enabled": True, "type": "remote", "url": spec.proxy_url}
|
|
1179
1179
|
if spec.headers:
|
|
1180
1180
|
entry["headers"] = spec.headers
|
|
1181
1181
|
return entry
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
from enum import Enum
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
|
|
@@ -5,7 +6,7 @@ import anyio
|
|
|
5
6
|
import structlog
|
|
6
7
|
import typer
|
|
7
8
|
|
|
8
|
-
from runlayer_cli.api import RunlayerClient
|
|
9
|
+
from runlayer_cli.api import RunlayerClient, SkillScanResponse
|
|
9
10
|
from runlayer_cli.console import print_error
|
|
10
11
|
from runlayer_cli.config import resolve_credentials, set_credentials_in_context
|
|
11
12
|
from runlayer_cli.logging import setup_logging
|
|
@@ -61,7 +62,7 @@ def _resolve_client(client_name: str | None) -> str:
|
|
|
61
62
|
return "claude_code"
|
|
62
63
|
|
|
63
64
|
|
|
64
|
-
def
|
|
65
|
+
def _load_skills(path: str) -> list[DiscoveredSkill]:
|
|
65
66
|
target = Path(path).resolve()
|
|
66
67
|
if not target.exists():
|
|
67
68
|
raise ValueError(f"Path does not exist: {target}")
|
|
@@ -73,14 +74,51 @@ def _load_single_skill(path: str) -> DiscoveredSkill:
|
|
|
73
74
|
else:
|
|
74
75
|
root = target
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
if not
|
|
77
|
+
skills = discover_skills(root)
|
|
78
|
+
if not skills:
|
|
78
79
|
raise ValueError(f"No skill found under {root}")
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return
|
|
80
|
+
return skills
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _scan_skill(client: RunlayerClient, skill: DiscoveredSkill) -> SkillScanResponse:
|
|
84
|
+
return client.score_skill(
|
|
85
|
+
skill_name=skill.name,
|
|
86
|
+
files=[
|
|
87
|
+
{
|
|
88
|
+
"name": file.title,
|
|
89
|
+
"content": file.content,
|
|
90
|
+
}
|
|
91
|
+
for file in skill.files
|
|
92
|
+
],
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _override_skill_name(skill: DiscoveredSkill, name: str) -> DiscoveredSkill:
|
|
97
|
+
return DiscoveredSkill(
|
|
98
|
+
path=skill.path,
|
|
99
|
+
name=name,
|
|
100
|
+
description=skill.description,
|
|
101
|
+
files=skill.files,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _render_scan_results(
|
|
106
|
+
scanned_skills: list[tuple[DiscoveredSkill, SkillScanResponse]],
|
|
107
|
+
) -> str:
|
|
108
|
+
if len(scanned_skills) == 1:
|
|
109
|
+
return scanned_skills[0][1].model_dump_json(indent=2)
|
|
110
|
+
|
|
111
|
+
payload = {
|
|
112
|
+
"skills": [
|
|
113
|
+
{
|
|
114
|
+
"path": skill.path,
|
|
115
|
+
"name": skill.name,
|
|
116
|
+
**result.model_dump(mode="json"),
|
|
117
|
+
}
|
|
118
|
+
for skill, result in scanned_skills
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
return json.dumps(payload, indent=2)
|
|
84
122
|
|
|
85
123
|
|
|
86
124
|
def _should_fail(risk_level: str, fail_on: FailOn | None) -> bool:
|
|
@@ -184,23 +222,20 @@ def scan(
|
|
|
184
222
|
credentials = resolve_credentials(ctx, require_auth=True, allow_org_key=True)
|
|
185
223
|
|
|
186
224
|
try:
|
|
187
|
-
|
|
188
|
-
|
|
225
|
+
skills = _load_skills(path)
|
|
226
|
+
if name is not None and len(skills) != 1:
|
|
227
|
+
raise ValueError("--name requires a single skill path")
|
|
228
|
+
if name is not None:
|
|
229
|
+
skills[0] = _override_skill_name(skills[0], name)
|
|
189
230
|
client = RunlayerClient(
|
|
190
231
|
hostname=credentials["host"], secret=credentials["secret"]
|
|
191
232
|
)
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
for file in skill.files
|
|
200
|
-
],
|
|
201
|
-
)
|
|
202
|
-
typer.echo(result.model_dump_json(indent=2))
|
|
203
|
-
if _should_fail(result.skill_risk_level, fail_on):
|
|
233
|
+
scanned_skills = [(skill, _scan_skill(client, skill)) for skill in skills]
|
|
234
|
+
typer.echo(_render_scan_results(scanned_skills))
|
|
235
|
+
if any(
|
|
236
|
+
_should_fail(result.skill_risk_level, fail_on)
|
|
237
|
+
for _, result in scanned_skills
|
|
238
|
+
):
|
|
204
239
|
raise typer.Exit(2 if fail_on == FailOn.WARN else 3)
|
|
205
240
|
except typer.Exit:
|
|
206
241
|
raise
|
|
@@ -380,14 +380,10 @@ def _build_plugin_mcp_config(
|
|
|
380
380
|
host: str,
|
|
381
381
|
client_name: str,
|
|
382
382
|
) -> dict[str, dict[str, dict[str, str]]]:
|
|
383
|
-
#
|
|
384
|
-
#
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
else:
|
|
388
|
-
client_def = get_client_by_name(client_name)
|
|
389
|
-
servers_key = client_def.servers_key if client_def else "mcpServers"
|
|
390
|
-
return {servers_key: _build_plugin_proxy_servers(plugin, host)}
|
|
383
|
+
# Plugin `.mcp.json` always uses the standard "mcpServers" key regardless
|
|
384
|
+
# of what key the client's own config format uses (e.g. VS Code uses
|
|
385
|
+
# "servers", Codex uses "mcp_servers" in TOML).
|
|
386
|
+
return {"mcpServers": _build_plugin_proxy_servers(plugin, host)}
|
|
391
387
|
|
|
392
388
|
|
|
393
389
|
def _write_plugin_mcp_json(
|
|
@@ -404,6 +404,40 @@ MCP_CLIENTS: list[MCPClientDefinition] = [
|
|
|
404
404
|
),
|
|
405
405
|
notes="Servers live under top-level 'mcp' and use OpenCode format (type=local|remote, command[]=..., environment={...}).",
|
|
406
406
|
),
|
|
407
|
+
MCPClientDefinition(
|
|
408
|
+
name="codex",
|
|
409
|
+
display_name="Codex",
|
|
410
|
+
paths=[
|
|
411
|
+
ConfigPath("~/.codex/config.toml", platform="macos"),
|
|
412
|
+
ConfigPath("~/.codex/config.toml", platform="linux"),
|
|
413
|
+
ConfigPath("%USERPROFILE%/.codex/config.toml", platform="windows"),
|
|
414
|
+
],
|
|
415
|
+
servers_key="mcp_servers",
|
|
416
|
+
config_format="toml",
|
|
417
|
+
project_config=ProjectConfigPattern(
|
|
418
|
+
relative_path=".codex/config.toml",
|
|
419
|
+
servers_key="mcp_servers",
|
|
420
|
+
),
|
|
421
|
+
plugin_paths=[
|
|
422
|
+
PluginPath(
|
|
423
|
+
"~/.codex/plugins/cache",
|
|
424
|
+
platform="macos",
|
|
425
|
+
mcp_filenames=("mcp.json", ".mcp.json"),
|
|
426
|
+
),
|
|
427
|
+
PluginPath(
|
|
428
|
+
"%USERPROFILE%/.codex/plugins/cache",
|
|
429
|
+
platform="windows",
|
|
430
|
+
mcp_filenames=("mcp.json", ".mcp.json"),
|
|
431
|
+
),
|
|
432
|
+
PluginPath(
|
|
433
|
+
"~/.codex/plugins/cache",
|
|
434
|
+
platform="linux",
|
|
435
|
+
mcp_filenames=("mcp.json", ".mcp.json"),
|
|
436
|
+
),
|
|
437
|
+
],
|
|
438
|
+
notes="TOML format. MCP servers under [mcp_servers.<name>] tables. "
|
|
439
|
+
"Plugins in ~/.codex/plugins/cache/<marketplace>/<plugin>/<version>/.",
|
|
440
|
+
),
|
|
407
441
|
# ==========================================================================
|
|
408
442
|
# DESCOPED FROM v0 - Add these in a future release
|
|
409
443
|
# ==========================================================================
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Discover MCP servers bundled inside installed Codex plugins.
|
|
2
|
+
|
|
3
|
+
Plugins are cached at ~/.codex/plugins/cache/<marketplace>/<plugin>/<version>/.
|
|
4
|
+
Each plugin may contain mcp.json or .mcp.json with server definitions either
|
|
5
|
+
under the "mcpServers" key or at root level.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import platform
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import structlog
|
|
17
|
+
|
|
18
|
+
from runlayer_cli.scan.config_parser import (
|
|
19
|
+
MCPClientConfig,
|
|
20
|
+
MCPServerConfig,
|
|
21
|
+
parse_plugin_mcp_file,
|
|
22
|
+
)
|
|
23
|
+
from runlayer_cli.scan.plugin_scanner import (
|
|
24
|
+
_version_sort_key,
|
|
25
|
+
compute_plugin_identifier,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
logger = structlog.get_logger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class DiscoveredCodexPlugin:
|
|
33
|
+
"""A Codex plugin found in the cache with MCP server definitions."""
|
|
34
|
+
|
|
35
|
+
name: str
|
|
36
|
+
config_path: Path
|
|
37
|
+
config_modified_at: str | None
|
|
38
|
+
servers: list[MCPServerConfig]
|
|
39
|
+
plugin_identifier: str | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _discover_codex_plugins(
|
|
43
|
+
plugin_cache_base: Path,
|
|
44
|
+
mcp_filenames: tuple[str, ...] = ("mcp.json", ".mcp.json"),
|
|
45
|
+
) -> list[DiscoveredCodexPlugin]:
|
|
46
|
+
"""Scan Codex plugin cache for plugins containing MCP configs.
|
|
47
|
+
|
|
48
|
+
Traverses <base>/<marketplace>/<plugin>/<version>/ looking for MCP files.
|
|
49
|
+
"""
|
|
50
|
+
if not plugin_cache_base.is_dir():
|
|
51
|
+
return []
|
|
52
|
+
|
|
53
|
+
discovered: list[DiscoveredCodexPlugin] = []
|
|
54
|
+
seen: set[str] = set()
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
for marketplace_dir in sorted(plugin_cache_base.iterdir()):
|
|
58
|
+
if not marketplace_dir.is_dir():
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
for plugin_dir in sorted(marketplace_dir.iterdir()):
|
|
62
|
+
if not plugin_dir.is_dir():
|
|
63
|
+
continue
|
|
64
|
+
plugin_name = plugin_dir.name
|
|
65
|
+
seen_key = f"{marketplace_dir.name}/{plugin_name}"
|
|
66
|
+
if seen_key in seen:
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
for version_dir in sorted(
|
|
70
|
+
plugin_dir.iterdir(), key=_version_sort_key, reverse=True
|
|
71
|
+
):
|
|
72
|
+
if not version_dir.is_dir():
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
for mcp_filename in mcp_filenames:
|
|
76
|
+
mcp_path = version_dir / mcp_filename
|
|
77
|
+
if not mcp_path.exists():
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
servers = parse_plugin_mcp_file(mcp_path, plugin_name)
|
|
81
|
+
if not servers:
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
mtime = mcp_path.stat().st_mtime
|
|
86
|
+
modified_at = datetime.fromtimestamp(
|
|
87
|
+
mtime, tz=timezone.utc
|
|
88
|
+
).isoformat()
|
|
89
|
+
except OSError:
|
|
90
|
+
modified_at = None
|
|
91
|
+
|
|
92
|
+
plugin_id = compute_plugin_identifier(version_dir)
|
|
93
|
+
discovered.append(
|
|
94
|
+
DiscoveredCodexPlugin(
|
|
95
|
+
name=plugin_name,
|
|
96
|
+
config_path=mcp_path,
|
|
97
|
+
config_modified_at=modified_at,
|
|
98
|
+
servers=servers,
|
|
99
|
+
plugin_identifier=plugin_id,
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
seen.add(seen_key)
|
|
103
|
+
logger.debug(
|
|
104
|
+
"Found Codex plugin MCP config",
|
|
105
|
+
plugin=plugin_name,
|
|
106
|
+
server_count=len(servers),
|
|
107
|
+
)
|
|
108
|
+
break # found mcp config in this version dir
|
|
109
|
+
else:
|
|
110
|
+
continue
|
|
111
|
+
break # use first version dir with an mcp config
|
|
112
|
+
|
|
113
|
+
except OSError as e:
|
|
114
|
+
logger.warning(
|
|
115
|
+
"Failed to scan Codex plugin cache",
|
|
116
|
+
path=str(plugin_cache_base),
|
|
117
|
+
error=str(e),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return discovered
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def scan_codex_plugins(
|
|
124
|
+
plugin_cache_base: Path | None = None,
|
|
125
|
+
) -> list[MCPClientConfig]:
|
|
126
|
+
"""Scan Codex plugin cache for bundled MCP servers.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
plugin_cache_base: Override path for testing (uses default if None)
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
List of MCPClientConfig entries for plugins with MCP servers.
|
|
133
|
+
"""
|
|
134
|
+
if plugin_cache_base is None:
|
|
135
|
+
if platform.system() == "Windows":
|
|
136
|
+
profile = os.environ.get("USERPROFILE")
|
|
137
|
+
if not profile:
|
|
138
|
+
return []
|
|
139
|
+
plugin_cache_base = Path(profile) / ".codex" / "plugins" / "cache"
|
|
140
|
+
else:
|
|
141
|
+
try:
|
|
142
|
+
plugin_cache_base = Path.home() / ".codex" / "plugins" / "cache"
|
|
143
|
+
except RuntimeError:
|
|
144
|
+
return []
|
|
145
|
+
|
|
146
|
+
plugins = _discover_codex_plugins(plugin_cache_base)
|
|
147
|
+
if not plugins:
|
|
148
|
+
return []
|
|
149
|
+
|
|
150
|
+
configurations: list[MCPClientConfig] = []
|
|
151
|
+
for plugin in plugins:
|
|
152
|
+
configurations.append(
|
|
153
|
+
MCPClientConfig(
|
|
154
|
+
client="codex",
|
|
155
|
+
config_path=str(plugin.config_path),
|
|
156
|
+
config_modified_at=plugin.config_modified_at,
|
|
157
|
+
servers=plugin.servers,
|
|
158
|
+
config_scope="plugin",
|
|
159
|
+
plugin_identifier=plugin.plugin_identifier,
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
logger.info(
|
|
163
|
+
"Found MCP servers in Codex plugin",
|
|
164
|
+
plugin=plugin.name,
|
|
165
|
+
server_count=len(plugin.servers),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return configurations
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import hashlib
|
|
6
6
|
import json
|
|
7
|
+
import sys
|
|
7
8
|
from dataclasses import dataclass, field
|
|
8
9
|
from datetime import datetime, timezone
|
|
9
10
|
from pathlib import Path
|
|
@@ -13,6 +14,11 @@ import json5
|
|
|
13
14
|
import structlog
|
|
14
15
|
import yaml
|
|
15
16
|
|
|
17
|
+
if sys.version_info >= (3, 11):
|
|
18
|
+
import tomllib
|
|
19
|
+
else:
|
|
20
|
+
import tomli as tomllib
|
|
21
|
+
|
|
16
22
|
if TYPE_CHECKING:
|
|
17
23
|
from runlayer_cli.scan.clients import MCPClientDefinition
|
|
18
24
|
|
|
@@ -120,6 +126,25 @@ def parse_plugin_mcp_entries(
|
|
|
120
126
|
return servers
|
|
121
127
|
|
|
122
128
|
|
|
129
|
+
def parse_plugin_mcp_file(mcp_path: Path, plugin_name: str) -> list[MCPServerConfig]:
|
|
130
|
+
"""Read and parse an mcp.json / .mcp.json from a plugin directory."""
|
|
131
|
+
try:
|
|
132
|
+
raw = json5.loads(mcp_path.read_text(encoding="utf-8"))
|
|
133
|
+
except (ValueError, OSError) as e:
|
|
134
|
+
logger.warning(
|
|
135
|
+
"Failed to parse plugin MCP config",
|
|
136
|
+
plugin=plugin_name,
|
|
137
|
+
path=str(mcp_path),
|
|
138
|
+
error=str(e),
|
|
139
|
+
)
|
|
140
|
+
return []
|
|
141
|
+
|
|
142
|
+
if not isinstance(raw, dict):
|
|
143
|
+
return []
|
|
144
|
+
|
|
145
|
+
return parse_plugin_mcp_entries(raw, plugin_name)
|
|
146
|
+
|
|
147
|
+
|
|
123
148
|
def _parse_goose_extension(name: str, config: dict[str, Any]) -> MCPServerConfig | None:
|
|
124
149
|
"""Parse a Goose extension entry from config file.
|
|
125
150
|
|
|
@@ -266,7 +291,8 @@ def _parse_opencode_mcp_server(
|
|
|
266
291
|
cmd = command_list[0] if isinstance(command_list[0], str) else None
|
|
267
292
|
if not cmd:
|
|
268
293
|
return None
|
|
269
|
-
|
|
294
|
+
filtered_args: list[str] = [a for a in command_list[1:] if isinstance(a, str)]
|
|
295
|
+
args: list[str] | None = filtered_args if filtered_args else None
|
|
270
296
|
environment = config.get("environment")
|
|
271
297
|
env = environment if isinstance(environment, dict) else None
|
|
272
298
|
server = MCPServerConfig(
|
|
@@ -303,6 +329,69 @@ def _parse_opencode_mcp_server(
|
|
|
303
329
|
return None
|
|
304
330
|
|
|
305
331
|
|
|
332
|
+
def _parse_codex_mcp_server(
|
|
333
|
+
name: str, config: dict[str, Any]
|
|
334
|
+
) -> MCPServerConfig | None:
|
|
335
|
+
"""Parse a Codex MCP server entry from config.toml.
|
|
336
|
+
|
|
337
|
+
Codex TOML format (docs: developers.openai.com/codex/mcp):
|
|
338
|
+
- enabled: bool (defaults to true)
|
|
339
|
+
- command + args + env for stdio
|
|
340
|
+
- url + http_headers / env_http_headers / bearer_token_env_var for streamable-http
|
|
341
|
+
"""
|
|
342
|
+
if config.get("enabled") is False:
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
if "url" in config:
|
|
346
|
+
url = config["url"]
|
|
347
|
+
if not isinstance(url, str) or not url:
|
|
348
|
+
return None
|
|
349
|
+
headers: dict[str, str] = {}
|
|
350
|
+
if isinstance(config.get("http_headers"), dict):
|
|
351
|
+
headers.update(config["http_headers"])
|
|
352
|
+
if isinstance(config.get("env_http_headers"), dict):
|
|
353
|
+
for header_name, env_var in config["env_http_headers"].items():
|
|
354
|
+
if isinstance(env_var, str):
|
|
355
|
+
headers[header_name] = f"${{{env_var}}}"
|
|
356
|
+
bearer_env = config.get("bearer_token_env_var")
|
|
357
|
+
if isinstance(bearer_env, str) and bearer_env:
|
|
358
|
+
headers["Authorization"] = f"Bearer ${{{bearer_env}}}"
|
|
359
|
+
server = MCPServerConfig(
|
|
360
|
+
name=name,
|
|
361
|
+
type="streamable-http",
|
|
362
|
+
command=None,
|
|
363
|
+
args=None,
|
|
364
|
+
url=url,
|
|
365
|
+
env=None,
|
|
366
|
+
headers=headers or None,
|
|
367
|
+
project_name=config.get("project_name"),
|
|
368
|
+
)
|
|
369
|
+
server.config_hash = compute_config_hash(server)
|
|
370
|
+
return server
|
|
371
|
+
|
|
372
|
+
command = config.get("command")
|
|
373
|
+
if not isinstance(command, str) or not command:
|
|
374
|
+
return None
|
|
375
|
+
args = config.get("args")
|
|
376
|
+
if args is not None and not isinstance(args, list):
|
|
377
|
+
args = None
|
|
378
|
+
env = config.get("env")
|
|
379
|
+
if env is not None and not isinstance(env, dict):
|
|
380
|
+
env = None
|
|
381
|
+
server = MCPServerConfig(
|
|
382
|
+
name=name,
|
|
383
|
+
type="stdio",
|
|
384
|
+
command=command,
|
|
385
|
+
args=args,
|
|
386
|
+
url=None,
|
|
387
|
+
env=env,
|
|
388
|
+
headers=None,
|
|
389
|
+
project_name=config.get("project_name"),
|
|
390
|
+
)
|
|
391
|
+
server.config_hash = compute_config_hash(server)
|
|
392
|
+
return server
|
|
393
|
+
|
|
394
|
+
|
|
306
395
|
def _parse_server_entry(name: str, config: dict[str, Any]) -> MCPServerConfig:
|
|
307
396
|
"""Parse a single server entry from config file.
|
|
308
397
|
|
|
@@ -413,14 +502,24 @@ def parse_config_file(
|
|
|
413
502
|
config_format = getattr(client_def, "config_format", "json")
|
|
414
503
|
|
|
415
504
|
try:
|
|
416
|
-
|
|
417
|
-
|
|
505
|
+
if config_format == "toml":
|
|
506
|
+
with open(config_path, "rb") as fb:
|
|
507
|
+
raw_config = tomllib.load(fb)
|
|
508
|
+
elif config_format == "yaml":
|
|
509
|
+
with open(config_path, encoding="utf-8") as f:
|
|
418
510
|
raw_config = yaml.safe_load(f)
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
# Many editors (VS Code, Zed, Cursor) use JSONC for config files
|
|
511
|
+
else:
|
|
512
|
+
with open(config_path, encoding="utf-8") as f:
|
|
422
513
|
content = f.read()
|
|
423
|
-
|
|
514
|
+
raw_config = json5.loads(content)
|
|
515
|
+
except tomllib.TOMLDecodeError as e:
|
|
516
|
+
logger.warning(
|
|
517
|
+
"Failed to parse config file - invalid TOML",
|
|
518
|
+
client=client_def.name,
|
|
519
|
+
path=str(config_path),
|
|
520
|
+
error=str(e),
|
|
521
|
+
)
|
|
522
|
+
return None
|
|
424
523
|
except yaml.YAMLError as e:
|
|
425
524
|
logger.warning(
|
|
426
525
|
"Failed to parse config file - invalid YAML",
|
|
@@ -430,7 +529,6 @@ def parse_config_file(
|
|
|
430
529
|
)
|
|
431
530
|
return None
|
|
432
531
|
except ValueError as e:
|
|
433
|
-
# json5 raises ValueError for parse errors
|
|
434
532
|
logger.warning(
|
|
435
533
|
"Failed to parse config file - invalid JSON/JSONC",
|
|
436
534
|
client=client_def.name,
|
|
@@ -484,6 +582,10 @@ def parse_config_file(
|
|
|
484
582
|
server = _parse_opencode_mcp_server(name, server_config)
|
|
485
583
|
if server is not None:
|
|
486
584
|
servers.append(server)
|
|
585
|
+
elif client_def.name == "codex":
|
|
586
|
+
server = _parse_codex_mcp_server(name, server_config)
|
|
587
|
+
if server is not None:
|
|
588
|
+
servers.append(server)
|
|
487
589
|
else:
|
|
488
590
|
server = _parse_server_entry(name, server_config)
|
|
489
591
|
servers.append(server)
|
|
@@ -20,7 +20,7 @@ from runlayer_cli.scan.clients import MCPClientDefinition
|
|
|
20
20
|
from runlayer_cli.scan.config_parser import (
|
|
21
21
|
MCPClientConfig,
|
|
22
22
|
MCPServerConfig,
|
|
23
|
-
|
|
23
|
+
parse_plugin_mcp_file,
|
|
24
24
|
)
|
|
25
25
|
from runlayer_cli.scan.plugin_scanner import compute_plugin_identifier
|
|
26
26
|
|
|
@@ -38,25 +38,6 @@ class DiscoveredPlugin:
|
|
|
38
38
|
plugin_identifier: str | None = None
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
def _parse_plugin_mcp_file(mcp_path: Path, plugin_name: str) -> list[MCPServerConfig]:
|
|
42
|
-
"""Parse an mcp.json or .mcp.json from a plugin directory."""
|
|
43
|
-
try:
|
|
44
|
-
raw = json5.loads(mcp_path.read_text(encoding="utf-8"))
|
|
45
|
-
except (ValueError, OSError) as e:
|
|
46
|
-
logger.warning(
|
|
47
|
-
"Failed to parse plugin MCP config",
|
|
48
|
-
plugin=plugin_name,
|
|
49
|
-
path=str(mcp_path),
|
|
50
|
-
error=str(e),
|
|
51
|
-
)
|
|
52
|
-
return []
|
|
53
|
-
|
|
54
|
-
if not isinstance(raw, dict):
|
|
55
|
-
return []
|
|
56
|
-
|
|
57
|
-
return parse_plugin_mcp_entries(raw, plugin_name)
|
|
58
|
-
|
|
59
|
-
|
|
60
41
|
def discover_plugins(client_def: MCPClientDefinition) -> list[DiscoveredPlugin]:
|
|
61
42
|
"""Scan plugin cache directories for plugins containing MCP configs.
|
|
62
43
|
|
|
@@ -91,7 +72,7 @@ def discover_plugins(client_def: MCPClientDefinition) -> list[DiscoveredPlugin]:
|
|
|
91
72
|
if not mcp_path.exists():
|
|
92
73
|
continue
|
|
93
74
|
|
|
94
|
-
servers =
|
|
75
|
+
servers = parse_plugin_mcp_file(mcp_path, plugin_name)
|
|
95
76
|
if not servers:
|
|
96
77
|
continue
|
|
97
78
|
|