RFC: Plugin Extension System — runtime intelligence, hosted MCP, and plugin ecosystem#1
Merged
ductiletoaster merged 25 commits intomainfrom Mar 22, 2026
Merged
Conversation
…xtension) Implements the full plugin extension system allowing third-party packages to contribute CLI commands and MCP tools to CGC via Python entry points. Core infrastructure: - PluginRegistry: discovers cgc_cli_plugins/cgc_mcp_plugins entry-point groups, validates PLUGIN_METADATA, enforces version constraints, isolates broken plugins - CLI integration: plugin commands attached to Typer app at import time; `cgc plugin list` - MCP server integration: plugin tools merged into tools dict; handlers routed at call time Plugins implemented: - cgc-plugin-stub: minimal reference fixture for testing and authoring examples - cgc-plugin-otel: gRPC OTLP receiver, Neo4j writer (Service/Trace/Span nodes, CORRELATES_TO links to static Method nodes), MCP query tools - cgc-plugin-xdebug: DBGp TCP listener, call-stack parser, dedup writer (StackFrame nodes, CALLED_BY chains, RESOLVES_TO Method links), dev-only - cgc-plugin-memory: MCP knowledge store (Memory/Observation nodes, DESCRIBES edges to Class/Method, FULLTEXT search, undocumented code queries) CI/CD and deployment: - GitHub Actions: plugin-publish.yml (matrix build/smoke-test/push via services.json), test-plugins.yml (PR plugin unit+integration tests) - Docker: docker-compose.plugin-stack.yml (self-contained full stack with Neo4j healthcheck + init.cypher), plugin/dev overlays, all plugin Dockerfiles - Kubernetes: otel-processor and memory plugin Deployment+Service manifests - Fixed docker-compose.template.yml: neo4j healthcheck was missing (broke all overlay depends_on: service_healthy conditions), init.cypher now mounted Tests (74 passing, 17 skipped pending plugin installs): - Unit: PluginRegistry, OTEL span processor, Xdebug DBGp parser - Integration: OTEL neo4j_writer, memory MCP handlers, plugin load isolation - E2E: full plugin lifecycle, broken plugin isolation, MCP server routing Documentation: - docs/plugins/authoring-guide.md: step-by-step plugin authoring with examples - docs/plugins/cross-layer-queries.md: 5 canonical cross-layer Cypher queries (SC-005) - docs/plugins/manual-testing.md: Docker and Python testing paths, per-plugin verification sequences, troubleshooting table - docs/plugins/examples/send_test_span.py: synthetic OTLP span sender for OTEL testing - .env.example: documented all plugin environment variables - CLAUDE.md: updated with plugin directories, entry-point groups, test commands Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CI/CD workflow fixes:
- e2e-tests.yml: add FalkorDB service container with health check, bump
checkout@v3->v4 and setup-python@v4->v5, add cache: pip, expose
FALKORDB_HOST/FALKORDB_PORT/DATABASE_TYPE env vars for test runner
- macos.yml: replace || true shell suppression with continue-on-error: true
on Install system deps, Run index, and Try find steps
- test-plugins.yml: remove silent pip fallback from core install step so
broken installs fail fast instead of silently proceeding with partial deps
- test.yml: add cache: pip to setup-python step
Application fixes (from python-pro):
- cgc.spec: PyInstaller frozen binary fixes
- tests/integration/plugin/test_otel_integration.py: asyncio fix
- tests/integration/plugin/test_memory_integration.py: importorskip guards
- tests/unit/plugin/test_otel_processor.py: importorskip guards
- tests/unit/plugin/test_xdebug_parser.py: importorskip guards
- plugins/cgc-plugin-{stub,otel,xdebug,memory}/README.md: plugin READMEs
- pyinstaller_hooks/: PyInstaller runtime hooks for plugin discovery
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…39+ compatibility falkordblite requires glibc 2.39+, which is not available in the manylinux2014_x86_64 image (glibc 2.17). Switch the Linux PyInstaller docker build to quay.io/pypa/manylinux_2_39_x86_64. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…s to __init__.py All four plugin pyproject.toml files declared entry points with a module:attribute suffix (e.g. cgc_plugin_stub.cli:get_plugin_commands), causing ep.load() to return a function rather than the module. The PluginRegistry calls ep.load() and reads PLUGIN_METADATA, get_plugin_commands, get_mcp_tools, and get_mcp_handlers off the returned object as module attributes — so loading always failed. Fix: remove the colon-attribute suffix so each entry point resolves to the package module (e.g. cgc_plugin_stub). Add re-exports of get_plugin_commands, get_mcp_tools, and get_mcp_handlers from the cli/mcp_tools submodules into each plugin's __init__.py so the registry finds them on the loaded module. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…dblite glibc 2.39 requirement Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…r Linux frozen binary
The Linux manylinux_2_39_x86_64 falkordblite wheel (produced by auditwheel)
installs vendored shared libraries into a `falkordblite.libs/` directory
(libcrypto, libssl, libgomp) that is not a Python package. collect_all() and
collect_dynamic_libs() only walk the importable top-level packages listed in
top_level.txt ('dummy', 'redislite') and therefore silently miss falkordblite.libs/,
causing runtime linker failures inside the PyInstaller one-file executable.
Changes:
- Add explicit search_paths scan for falkordblite.libs/*.so* and register each
file as a binary with destination 'falkordblite.libs'
- Collect dummy.cpython-*.so (auditwheel sentinel extension) from site-packages
roots and register it at the bundle root
- Add 'dummy' to hidden_imports (non-Windows) to cover the importable top-level
package declared in falkordblite's top_level.txt
pyproject.toml dependency marker (sys_platform != 'win32' and python_version >= '3.12')
is correct — no change needed there.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Generic project knowledge/memory is well-served by existing ecosystem tools (mem0, Neo4j memory server, etc.). Removing the memory plugin keeps CGC focused on its differentiator: code graph intelligence combining static structure with runtime behavior. Removes: cgc-plugin-memory package, K8s manifests, integration tests, docker-compose services, Neo4j schema indexes, CI/CD matrix entries, and all spec/doc references. Replaces memory-dependent cross-layer queries with runtime-only equivalents (never_observed). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The gRPC server was hitting an AttributeError on grpc.experimental.insecure_channel_credentials() — dead code from an earlier iteration that was still evaluated despite the `if False` guard (Python evaluates both sides of a ternary). Replaced with a clean ThreadPoolExecutor-based grpc.server() call. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… process The Dockerfile CMD runs `python -m cgc_plugin_xdebug.dbgp_server` but the module had no main() or __main__ block, so the container silently exited. Adds a main() that initializes the DB connection, creates the writer, and starts the DBGp TCP listener with graceful SIGTERM shutdown. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…dation Three sample apps (PHP/Laravel, Python/FastAPI, TypeScript/Express gateway) demonstrating the full CGC pipeline: index code → run instrumented app → generate OTEL spans → query cross-layer graph. Includes shared docker-compose, automated smoke script with 7 Cypher assertions, and E2E test wrapper. Documents the FQN correlation gap as a known limitation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- PHP: add libsqlite3-dev, install ext-opentelemetry via PECL, remove post-autoload-dump scripts, create bootstrap/cache and storage dirs - Python: remove non-existent opentelemetry-instrumentation-aiosqlite, fix trailing-slash redirects by using empty string routes - TypeScript: fix TS2322 type error in dashboard-service.ts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…based workflow - receiver.py: capture main event loop in __init__ instead of calling asyncio.get_event_loop() from gRPC thread pool - neo4j_writer.py: use sync Neo4j sessions (DatabaseManager provides sync driver) - docker-compose.plugin-stack.yml: add DATABASE_TYPE=neo4j to otel-processor env - smoke-all.sh: use cgc-core-indexer container instead of requiring local cgc - README.md: full Docker-based quick start (no local install required) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Brings Neo4j up to the current stable release with modernized browser UI. Updates dbms.* config keys to server.* namespace (2025+ format). Applied across plugin-stack, template, and k8s deployment manifests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add `indexer` service to samples/docker-compose.yml with profiles: [indexer] so it only runs on-demand via `docker compose run --rm indexer`. No local CGC install needed — the full workflow is now: cd samples/ docker compose up -d --build docker compose run --rm indexer bash smoke-all.sh Update smoke-all.sh to use the indexer service instead of probing for local cgc or a manually started container. Simplify README quick start. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The blanket `docker-compose.yml` pattern was ignoring samples/docker-compose.yml. Scope to `/docker-compose.yml` so only the root file (generated per-environment) is ignored. Named compose files and samples/ compose are tracked normally. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rvice Node's built-in fetch uses undici which OTEL HttpInstrumentation can't intercept. Switch proxy-service and dashboard-service to use Node's http module. Add requestHook to HttpInstrumentation that maps target hostnames to peer.service attributes, enabling CALLS_SERVICE edge formation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Documents findings from querying the populated graph as an AI assistant discovering the codebase for the first time. Covers what works (static call graph, runtime service topology, cross-service tracing, distributed trace linking) and what doesn't (cross-layer correlation due to FQN gap and missing code attributes in auto-instrumentation spans). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extends plan, spec, and tasks with US6 (P6): HTTP transport for the MCP server, API key auth, CORS, /healthz endpoint, and a dedicated container image deployable to Docker/Swarm/K8s. Adds FR-039–FR-047, SC-012, Phase 8 tasks (T069–T080), and updated dependency graph. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Clarifications from /speckit.clarify session: - Plain JSON-RPC request/response (no SSE/streaming) - Single-process async (uvicorn default event loop) - No app-level auth (defer to reverse proxy) - /healthz returns 503 when DB unreachable Updates plan.md (HTTP transport deps, constraints, project structure), quickstart.md (hosted MCP section), and tasks.md (all 72 tasks checked). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract handle_request() from server.py stdio loop, implement HTTPTransport class (FastAPI + uvicorn) with POST /mcp and GET /healthz endpoints, add --transport option to cgc mcp start CLI command. - POST /mcp: plain JSON-RPC request/response dispatch - GET /healthz: 200 ok / 503 unhealthy based on DB connectivity - CORS via CGC_CORS_ORIGIN env var (default *) - Port via CGC_MCP_PORT env var (default 8045) - 23 tests (12 unit + 11 integration) all passing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Dockerfile.mcp: multi-stage build, non-root user, core + all plugins - docker-compose.plugin-stack.yml: cgc-mcp service on port 8045 - k8s/cgc-mcp/: Deployment + ClusterIP Service with /healthz probes - .github/services.json: add cgc-mcp to CI/CD matrix build Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- docs/deployment/MCP_SERVER_HOSTING.md: deployment guide with client config snippets for Claude Desktop, VS Code, Cursor, Claude Code - tests/e2e/test_mcp_container.py: Docker-based E2E tests (skipped when Docker unavailable) - samples/docker-compose.yml: cgc-mcp behind mcp profile - samples/README.md: hosted MCP server section Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this proposes
A plugin extension system for CodeGraphContext that enables independently installable packages to contribute CLI commands and MCP tools — without modifying core CGC code. The system ships with two first-party plugins (OTEL span processor and Xdebug trace listener), a shared CI/CD pipeline, sample applications, and a hosted MCP server container image.
Why
CGC currently provides static code graph analysis. This proposal extends it with runtime intelligence — the ability to overlay execution traces (OTEL spans, Xdebug call stacks) onto the static code graph, enabling cross-layer queries like "which methods executed during this request that have no test coverage" or "show code paths never observed at runtime."
The plugin architecture ensures this complexity is opt-in and composable: install only what you need, and broken plugins never crash the host.
Scope of changes
140 files changed, ~14,000 lines added, ~86 lines modified in existing files.
The change footprint on existing code is deliberately minimal:
src/)plugin_registry.py(new),http_transport.py(new),server.py(refactoredhandle_request()),cli/main.py(plugin loading +--transportoption)plugins/)cgc-plugin-stub(test fixture),cgc-plugin-otel(OTEL spans),cgc-plugin-xdebug(Xdebug traces)tests/)samples/)pyproject.toml(addedpackagingdep + plugin extras),.gitignore(scoped docker-compose ignore)What it does NOT change
Architecture overview
Plugin discovery: Uses Python
importlib.metadataentry-points (cgc_cli_plugins+cgc_mcp_plugins). Install a plugin withpip install cgc-plugin-otel→ CGC discovers it automatically at startup.Isolation: Each plugin load is wrapped in try/except with a 5-second SIGALRM timeout. A broken plugin logs a warning; CGC and all other plugins continue normally.
HTTP transport:
cgc mcp start --transport httpexposes a JSON-RPC endpoint atPOST /mcpwith a health check atGET /healthz. No auth at the application layer — designed to sit behind a reverse proxy.The 6 user stories
Full specification:
specs/001-cgc-plugin-extension/spec.mdProposed PR decomposition
If this direction is accepted, this branch could be broken into 6 independently reviewable PRs following the user story boundaries:
PR 1: Plugin Foundation (US1) — ~800 lines
The core infrastructure. Smallest possible reviewable unit.
src/codegraphcontext/plugin_registry.pyserver.pyandcli/main.pyplugins/cgc-plugin-stub/(test fixture)pyproject.tomlchangesPR 2: OTEL Plugin (US2) — ~1,500 lines
Depends on PR 1.
plugins/cgc-plugin-otel/(span processor, gRPC receiver, Neo4j writer)config/neo4j/init.cypher(schema),config/otel-collector/config.yamlPR 3: Xdebug Plugin (US3) — ~1,000 lines
Depends on PR 1. Independent of PR 2.
plugins/cgc-plugin-xdebug/(DBGp server, frame writer)PR 4: CI/CD Pipeline (US4) — ~500 lines
Depends on PRs 2+3 (needs Dockerfiles).
.github/workflows/(docker-publish, test-plugins).github/services.jsonPR 5: Sample Applications (US5) — ~2,500 lines
Depends on PR 2. Could be reviewed independently as reference material.
samples/(PHP/Laravel, Python/FastAPI, TypeScript/Express)smoke-all.shvalidation scriptKNOWN-LIMITATIONS.mdPR 6: Hosted MCP Server (US6) — ~2,000 lines
Depends on PR 1 only. Can be reviewed in parallel with PRs 2-5.
src/codegraphcontext/http_transport.pyDockerfile.mcp, K8s manifests, docker-compose additionsdocs/deployment/MCP_SERVER_HOSTING.mdStandalone: Specs & Documentation — ~3,000 lines
Could land first or alongside PR 1 as context.
specs/001-cgc-plugin-extension/(spec, plan, research, data model, contracts, tasks)docs/plugins/(authoring guide, cross-layer queries, manual testing)How to explore this branch
Open questions for maintainers
Test plan
docker compose up)samples/smoke-all.sh)🤖 Generated with Claude Code