Skip to content

RFC: Plugin Extension System — runtime intelligence, hosted MCP, and plugin ecosystem#1

Merged
ductiletoaster merged 25 commits intomainfrom
001-cgc-plugin-extension
Mar 22, 2026
Merged

RFC: Plugin Extension System — runtime intelligence, hosted MCP, and plugin ecosystem#1
ductiletoaster merged 25 commits intomainfrom
001-cgc-plugin-extension

Conversation

@ductiletoaster
Copy link
Copy Markdown
Member

@ductiletoaster ductiletoaster commented Mar 15, 2026

This PR is an RFC / proposal. The intent is to highlight a possible new direction for CGC and gather feedback on whether this is something the project is interested in pursuing. Nothing here needs to be merged as-is.

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:

Area Files What changed
Core CGC (src/) 4 files plugin_registry.py (new), http_transport.py (new), server.py (refactored handle_request()), cli/main.py (plugin loading + --transport option)
Plugins (plugins/) 23 files 3 plugin packages: cgc-plugin-stub (test fixture), cgc-plugin-otel (OTEL spans), cgc-plugin-xdebug (Xdebug traces)
Tests (tests/) 12 files Unit, integration, and E2E tests for plugin registry, OTEL processor, Xdebug parser, HTTP transport, container lifecycle
Samples (samples/) ~40 files 3 sample apps (PHP/Laravel, Python/FastAPI, TypeScript/Express) + smoke script + docker-compose
Infrastructure ~15 files Dockerfiles, docker-compose, K8s manifests, CI/CD workflows, Neo4j schema
Specs & docs ~30 files Feature specification, implementation plan, research, data model, contracts, plugin authoring guide, deployment guide
Root config 2 files pyproject.toml (added packaging dep + plugin extras), .gitignore (scoped docker-compose ignore)

What it does NOT change

  • No existing tests are modified or broken
  • No existing CLI commands are altered
  • The MCP stdio transport is completely unchanged (backwards compatible)
  • No existing graph schema is modified (new node types are additive)

Architecture overview

┌─────────────────────────────────────────────────────────┐
│                    CGC Core                              │
│  ┌──────────────┐  ┌──────────────┐  ┌───────────────┐  │
│  │PluginRegistry│  │  MCPServer   │  │ HTTPTransport │  │
│  │ (discovery)  │→ │ (tools/call) │← │ (POST /mcp)   │  │
│  └──────┬───────┘  └──────────────┘  └───────────────┘  │
│         │ entry-points                                   │
├─────────┼───────────────────────────────────────────────┤
│         ▼                                                │
│  ┌─────────────┐  ┌──────────────┐  ┌───────────────┐   │
│  │  stub plugin │  │  OTEL plugin │  │ Xdebug plugin │   │
│  │  (testing)   │  │  (runtime)   │  │  (dev traces) │   │
│  └─────────────┘  └──────────────┘  └───────────────┘   │
└─────────────────────────────────────────────────────────┘
                          │
                    ┌─────▼─────┐
                    │  Neo4j /  │
                    │ FalkorDB  │  ← Static + Runtime nodes
                    └───────────┘

Plugin discovery: Uses Python importlib.metadata entry-points (cgc_cli_plugins + cgc_mcp_plugins). Install a plugin with pip 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 http exposes a JSON-RPC endpoint at POST /mcp with a health check at GET /healthz. No auth at the application layer — designed to sit behind a reverse proxy.

The 6 user stories

# Story Priority Status
US1 Plugin Extensibility Foundation P1 (MVP) Complete
US2 Runtime Intelligence via OTEL Plugin P2 Complete
US3 Development Traces via Xdebug Plugin P3 Complete
US4 Automated Container Builds (CI/CD) P4 Complete
US5 Sample Applications (E2E validation) P5 Complete
US6 Hosted MCP Server Container Image P6 Complete

Full specification: specs/001-cgc-plugin-extension/spec.md

Proposed 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.py
  • Modifications to server.py and cli/main.py
  • plugins/cgc-plugin-stub/ (test fixture)
  • Unit + integration tests for plugin discovery/loading
  • pyproject.toml changes

PR 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.yaml
  • Docker services (docker-compose additions)
  • Unit + integration tests

PR 3: Xdebug Plugin (US3) — ~1,000 lines

Depends on PR 1. Independent of PR 2.

  • plugins/cgc-plugin-xdebug/ (DBGp server, frame writer)
  • Docker dev service
  • Unit tests

PR 4: CI/CD Pipeline (US4) — ~500 lines

Depends on PRs 2+3 (needs Dockerfiles).

  • .github/workflows/ (docker-publish, test-plugins)
  • .github/services.json
  • Dockerfiles for plugins
  • K8s manifests for OTEL plugin

PR 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.sh validation script
  • KNOWN-LIMITATIONS.md

PR 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.py
  • Dockerfile.mcp, K8s manifests, docker-compose additions
  • docs/deployment/MCP_SERVER_HOSTING.md
  • Unit + integration + E2E tests

Standalone: 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

# Clone and checkout
git checkout 001-cgc-plugin-extension

# Set up local dev environment
uv venv --python 3.12 .venv && source .venv/bin/activate
uv pip install -e ".[dev]" -e plugins/cgc-plugin-stub

# Run plugin tests (no database needed)
PYTHONPATH=src pytest tests/unit/plugin/ tests/unit/test_http_transport.py -v

# Run the full stack with Docker
docker compose -f docker-compose.plugin-stack.yml up -d
# Then: http://localhost:7474 (Neo4j Browser)
# Then: curl http://localhost:8045/healthz (MCP server health)

Open questions for maintainers

  1. Is runtime intelligence in scope? The core value proposition is overlaying execution data onto the static graph. Is this a direction the project wants to go?
  2. Plugin architecture vs. monorepo modules? The entry-points approach enables third-party plugins, but if extensibility isn't a goal, simpler module imports might suffice.
  3. Graph database assumptions — The OTEL and Xdebug plugins assume Neo4j with specific schema. Should they also support FalkorDB?
  4. Sample apps — Are PHP/Laravel + Python/FastAPI + TypeScript/Express the right choices for demonstrating the pipeline, or would different stacks be more relevant?

Test plan

  • All 23 HTTP transport tests pass (unit + integration)
  • All 57 plugin tests pass (registry, OTEL processor, Xdebug parser, plugin lifecycle)
  • Existing CGC tests unaffected
  • Docker-based E2E tests (require docker compose up)
  • Full sample app smoke script (samples/smoke-all.sh)

🤖 Generated with Claude Code

ductiletoaster and others added 25 commits March 14, 2026 19:50
…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>
@ductiletoaster ductiletoaster changed the title 001 cgc plugin extension RFC: Plugin Extension System — runtime intelligence, hosted MCP, and plugin ecosystem Mar 20, 2026
@ductiletoaster ductiletoaster merged commit 6b77821 into main Mar 22, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant