AGENTS.md

January 9, 2026 · View on GitHub

Guidelines for AI coding assistants working with this repository.

For domain-specific guidance, see subdirectory AGENTS.md files:

  • tests/AGENTS.md - Testing conventions and workflows
  • plugins/AGENTS.md - Plugin framework and development
  • charts/AGENTS.md - Helm chart operations
  • docs/AGENTS.md - Documentation authoring
  • mcp-servers/AGENTS.md - MCP server implementation
  • crates/mcp_runtime/DEVELOPING.md - Rust MCP runtime development workflows, command matrix, and validation

Note: The llms/ directory contains guidance for LLMs using ContextForge solution (end-user runtime guidance), not for code agents working on this codebase.

Project Overview

ContextForge is an open source registry and proxy that federates MCP, A2A, and REST/gRPC APIs with centralized governance, discovery, and observability. It federates tools, agents, and APIs, optimizes agent and tool calling, and supports plugins, auth/RBAC, rate-limiting, virtual servers, multi-transport protocols, and an optional Admin UI.

Project Structure

mcpgateway/                 # Core FastAPI application
├── main.py                 # Application entry point
├── config.py               # Environment configuration
├── db.py                   # SQLAlchemy ORM models and session management
├── schemas.py              # Pydantic validation schemas
├── services/               # Business logic layer (50+ services)
├── routers/                # HTTP endpoint definitions (19 routers)
├── middleware/             # Cross-cutting concerns (16 middleware)
├── transports/             # Protocol implementations (SSE, WebSocket, stdio, streamable HTTP)
├── plugins/                # Plugin integration (uses cpex external package)
└── alembic/                # Database migrations

tests/                      # Test suite (see tests/AGENTS.md)
plugins/                    # Plugin implementations (see plugins/AGENTS.md)
plugins_rust/               # Rust plugin implementations for performance-sensitive paths
charts/                     # Helm charts (see charts/AGENTS.md)
docs/                       # Architecture and usage documentation (see docs/AGENTS.md)
a2a-agents/                 # A2A agent implementations (used for testing/examples)
mcp-servers/                # MCP server templates (see mcp-servers/AGENTS.md)
crates/                     # Direct Rust crate folders (runtime and wrapper)
llms/                       # End-user LLM guidance (not for code agents)

Essential Commands

Setup

cp .env.example .env && make install-dev check-env    # Complete setup
make venv                          # Create virtual environment with uv
make install-dev                   # Install with dev dependencies (includes build-ui)
make check-env                     # Verify .env against .env.example
make build-ui                      # Rebuild Admin UI JS bundle (requires npm)

Development

make dev                          # Dev server on :8000 with autoreload
make serve                        # Production gunicorn on :4444
make serve-ssl                    # HTTPS on :4444 (creates certs if needed)

Code Quality

# After writing code
make pre-commit

# Before committing, use ty, mypy and pyrefly to check just the new files, then run:
make ruff bandit interrogate pylint verify

# Before committing Rust changes (tools_rust/):
# Run fmt-check, clippy -D warnings, and cargo test for Rust crates
cd tools_rust/mcp_runtime && cargo fmt --check && cargo clippy -- -D warnings && cargo test

PR Review Workflow

Standard prompt: "Rebase against main, then conduct an in-depth PR Review." It runs as a fixed-point loop terminating when a full pass surfaces no blocking findings. Each cycle clears blocking and functionally-impacting findings (within reason — escalate edge cases). Cosmetic suggestions can be deferred.

Review State: llms/NOTES.md

llms/NOTES.md is the ephemeral per-review cycle tracker — lives in the llms/ directory alongside other LLM guidance, persists across cycles within one review but never across reviews. It holds the cycle counter, the Conducting review / Implementing suggestions phase toggles, and per-gate checkboxes; update as you advance. The canonical, committed source is llms/NOTES.template.md. When starting a review, instantiate from the template:

cp llms/NOTES.template.md llms/NOTES.md

If llms/NOTES.md is missing or stale, reset from the template; never assume prior cycle state carries over.

Rebase

(cd ../mcp-context-forge && git pull)
git rebase main

For each conflict, apply the resolution that best preserves both intents using the PR diff and recent main commits as context. Escalate when the conflict is semantic (logic intent on both sides), would silently weaken a test / security check / migration, or when you're not confident the fix matches the PR author's intent. Preserve sign-off (git commit -s) on any new commits.

For conflicts with .secrets.baseline, use the version of .secrets.baseline from the main branch.

Cycle: review → fix → loop

  1. Verify scope. Cross-reference the PR description against any linked issues (gh pr view, gh issue view) and confirm the changes deliver what the PR claims. Partial coverage of an issue is acceptable when the PR documentation explicitly says so; unstated gaps or scope drift are blocking findings.
  2. Review. Default categorization: blocking / functionally-impacting / suggestions / minor (matches Tone for GitHub Comments). Output format and delivery channel (file, agent toolkit, gh pr review) decided per-PR — confirm before posting externally.
  3. Fix. Address every blocking and functionally-impacting finding this cycle. Update llms/NOTES.md to advance state.
  4. Loop. Repeat until a full pass yields zero blocking findings, then run the validation gate.

Pre-Merge Validation Gate

Run from the worktree root, in order. Each must pass (or have a documented waiver) before the PR is ready:

#CommandValidates
1make ruff interrogate pylintLint, docstring coverage, deeper static analysis
2make testFull pytest suite
3make coverage diff-coverCoverage of changed lines vs. base
4make docker-nuke docker-prod-rust testing-up RUST_MCP_MODE=Rebuilds and launches the production-style gateway stack
5make test-mcp-protocol-e2e test-mcp-rbacMCP protocol E2E and RBAC against the live gateway
6make detect-secrets-scanNo new secrets in files changed vs main; exits non-zero on live/unaudited findings (jq merge preserves out-of-scope audited entries; remediate with make detect-secrets-audit)

Distinct from the per-edit hygiene chain in Essential Commands → Code Quality (make autoflake isort black pre-commit, then make ruff bandit interrogate pylint verify): hygiene runs continuously; this gate runs once before declaring a PR ready.

Secret Detection (detect-secrets)

When detect-secrets identifies false positives:

  • Python files: Suppress inline using # pragma: allowlist secret comment so they don't appear in .secrets.baseline after running make detect-secrets-scan
    • The one exception to this is doctest strings where the content contains a false positive secret as the comment will interfere with assertions. In this case rely on the .secrets.baseline file and audit the result with 'make detect-secrets-audit'.
  • All other file types: Regenerate the baseline using make detect-secrets-scan to update .secrets.baseline

Authentication & RBAC Overview

ContextForge implements a two-layer security model:

  1. Token Scoping (Layer 1): Controls what resources a user CAN SEE (data filtering)
  2. RBAC (Layer 2): Controls what actions a user CAN DO (permission checks)

Token Scoping Quick Reference

API / legacy tokens — JWT teams claim is the sole authority (normalize_token_teams()):

JWT teams Stateis_admin: trueis_admin: false
Key MISSINGPUBLIC-ONLY []PUBLIC-ONLY []
teams: nullADMIN BYPASSPUBLIC-ONLY []
teams: []PUBLIC-ONLY []PUBLIC-ONLY []
teams: ["t1"]Team + PublicTeam + Public

Session tokens (token_use: "session") — DB is the authority; JWT teams only narrows (resolve_session_teams()):

JWT teams StateDB admin?ResultAccess Level
anyyesNoneADMIN BYPASS (DB authority)
Missing/null/[]noDB teamsFull DB membership
["t1"]nointersectionNarrowed to overlap
["revoked"]no[]Public-only (fail-closed)

Key behaviors:

  • API/legacy tokens: Missing teams key = public-only access (secure default). Admin bypass requires BOTH teams: null AND is_admin: true. normalize_token_teams() in mcpgateway/auth.py is the single source of truth.
  • Session tokens: Admin bypass is determined by the DB is_admin flag, not the JWT teams claim. Non-admin sessions can be narrowed via JWT teams. resolve_session_teams() in mcpgateway/auth.py is the single policy point.
  • Layer 1 only: Token scoping controls visibility (what you can see). RBAC (Layer 2) is evaluated independently — session-token narrowing does not restrict which team roles are checked for permissions.
  • External IdP tokens: identities provisioned from trusted external SSO providers (see SSO_API_TOKEN_AUTH_ENABLED) are dispatched through the session-token table above (resolve_session_teams()), not the API/legacy table — is_admin/teams come from the persisted local user record, never from the external token's claims.

Implementation Helpers

Layer-1 derivation is centralized in mcpgateway/auth_context.py. Route handlers must call these rather than re-deriving the rule inline:

  • get_scoped_resource_access_context(request, user)(user_email, token_teams) — the visibility scope to pass into service fetch/list/read calls. Admin bypass is signalled by token_teams=None while user_email is kept, so the service can still owner-match the admin's own private rows. Public-only is (email, []).
  • get_request_identity(request, user)(user_email, is_admin) — the requester's own identity, for audit capture and header masking. Use when you need the identity independent of the visibility scope.
  • get_rpc_filter_context(request, user)(user_email, token_teams, is_admin) — the raw pre-rule triple. Reserved for the few sites that genuinely need is_admin or the un-normalized teams (auth-context forwarding, run-ownership capture, tool-execution authorization); these carry a Layer-1 exception comment naming the reason.

The derived triple is memoized on request.state per principal, so calling the scope and identity helpers together costs one derivation rather than two.

Security Invariants (Required)

  • Treat public as platform-public scope, not internet-anonymous scope.
  • Explicit exception: when MCP_REQUIRE_AUTH=false, unauthenticated /mcp requests are allowed with public-only visibility — unless the target virtual server has oauth_enabled=True, in which case unauthenticated requests are rejected with 401 regardless of the global setting.
  • Keep the two-layer model on every path:
    • Layer 1: token scoping controls what a caller can see.
    • Layer 2: RBAC controls what a caller can do.
  • Do not re-implement token team interpretation logic; use normalize_token_teams() for API/legacy tokens and resolve_session_teams() for session tokens (both in mcpgateway/auth.py).
  • Do not re-implement Layer 1 token scope semantics; use token_scope_grants() in mcpgateway/middleware/rbac.py, the single policy point shared by the RBAC decorators and TokenScopingMiddleware. Empty token scopes mean "inherit from RBAC at runtime" (what TokenCatalogService._generate_token() emits for tokens created without an explicit scope) and must never be treated as deny-all; * grants everything and <category>.* grants that category.
  • Do not accept inbound client auth tokens via URL query parameters.
  • Legacy INSECURE_ALLOW_QUERYPARAM_AUTH is interop-only for outbound peer auth and must remain opt-in and host-restricted.
  • High-risk transports must be feature-flagged and disabled by default.
  • Transport/session endpoints must authenticate before session establishment (or message forwarding) and enforce RBAC before processing actions.
  • Token-scoped route authorization must be default-deny for unmapped protected paths.
  • Never trust client-provided ownership fields (owner_email, team_id, session owner); derive authorization from authenticated identity and server-side state.
  • Security-sensitive changes must include deny-path regression tests (unauthenticated, wrong team, insufficient permissions, feature disabled).
  • A token-exchange OAuth grant (RFC 8693 / On-Behalf-Of) exists for gateways; with it, the user's inbound JWT is exchanged with a trusted Authorization Server and never forwarded upstream — only the exchanged token is sent to the downstream MCP server.
  • token_url on a token-exchange gateway is an SSRF / egress boundary: the user's ContextForge JWT is POSTed to it as the subject_token, it is validated at config time, and creating or modifying token-exchange gateways is a privileged action.
  • Audit token-exchange operations via the structured logging sink with a correlation_id; never log raw subject tokens or exchanged tokens.

UAID Cross-Gateway Security

  • UAID cross-gateway routing requires explicit domain allowlist (fail-closed default)
  • Empty UAID_ALLOWED_DOMAINS blocks all cross-gateway routing unless UAID_ALLOW_ALL_DOMAINS=true
  • Cross-gateway calls forward bearer tokens for RBAC enforcement on remote gateways
  • Both gateways must trust the same JWT issuer (shared JWT_SECRET_KEY or federated SSO)
  • UAID_ALLOW_ALL_DOMAINS=true is unsafe for production (bypasses allowlist validation)
  • Startup validation logs ERROR if A2A enabled but allowlist not configured
  • Remote gateway 401/403 responses raise A2AAgentError with troubleshooting guidance

Built-in Roles

RoleScopeKey Permissions
platform_adminglobal* (all)
team_adminteamteams.*, tools.read/execute, resources.read
developerteamtools.read/execute, resources.read
viewerteamtools.read/execute, resources.read

Documentation

  • Full RBAC guide: docs/docs/manage/rbac.md
  • Multi-tenancy architecture: docs/docs/architecture/multitenancy.md
  • OAuth token delegation: docs/docs/architecture/oauth-design.md

User Identity Extraction

Canonical Email Precedence: All user-email extraction helpers use a consistent email-over-sub precedence order to ensure forensic accuracy across visibility checks and audit logs:

  • When a user dict contains both email and sub keys, email takes precedence
  • The canonical implementation is get_user_email() in mcpgateway/auth_context.py
  • All other helpers (including admin.get_user_email) re-export or delegate to this canonical implementation
  • This ensures that the identity used for RBAC evaluation matches the identity logged in audit trails

Rationale: The email field is the human-readable identifier used throughout AGENTS.md and user-facing documentation. Consistent precedence prevents forensic confusion where an incident review pivots on a logged email that differs from the principal actually evaluated by RBAC.

Observability Transaction Behavior

Issue #3883 - Separate Session Pattern

Observability write operations use independent database sessions that commit immediately (best-effort pattern). This means:

  • Observability data persists even when the main request fails
  • Traces may show "in progress" or partial states for failed requests
  • NOT atomic with main request transaction (intentional trade-off)
  • Provides visibility into partial failures at the cost of atomicity

Implementation Details

Write methods (use independent sessions):

  • start_trace(), end_trace()
  • start_span(), end_span()
  • add_event(), record_token_usage(), record_metric(), delete_old_traces()

Query methods (use request-scoped sessions):

  • get_trace(), get_traces(), get_spans(), etc.
  • These accept a db: Session parameter for RBAC/token scoping

Context managers (create single independent session for lifecycle):

  • trace_span(), trace_tool_invocation(), trace_a2a_request()

Pattern: Follows existing SQL instrumentation approach in instrumentation/sqlalchemy.py:58-87

Audit Trail Transaction Behavior

Issue #2871 - Separate Session Pattern

AuditTrailService.log_action() (mcpgateway/services/audit_trail_service.py) always opens its own SessionLocal() when no db is supplied, and closes/rolls back that session itself. Callers in tool_service.py, resource_service.py, gateway_service.py, prompt_service.py, server_service.py, and admin.py must never pass db=db (the caller's request-scoped session) to log_action(). In admin.py, plugin-view audit logging goes through log_audit(), which is a thin wrapper over log_action() and inherits the same optional-session behavior.

Passing the shared session caused "This transaction is inactive" errors: the main CRUD operation already calls db.commit() before the audit call, and reusing that same session for a second commit after it has already committed leaves the session in a state that breaks rollback on subsequent errors.

  • log_action() swallows its own exceptions internally and returns None on failure — it does not propagate them to the caller in production.
  • The main resource (tool/gateway/resource/prompt/server) is committed before log_action() runs, so an audit-logging failure never rolls back already-persisted data — but callers' generic except Exception handlers still call db.rollback() and surface an error to the API caller even though the underlying row was already committed.
  • Do not add db: Session back to a log_action() call site. If a service needs the audit entry guaranteed within the same transaction as the main write, that is a deliberate design change requiring review, not a default.

Middleware: ObservabilityMiddleware no longer creates request.state.db. Each observability operation creates its own short-lived session.

Security: Query operations use request-scoped sessions for RBAC/token scoping. Write operations are not RBAC-protected (observability visibility is platform-wide).

Connection Pool Sizing: The separate session pattern creates 4-6 independent database sessions per traced request (trace start/end, span start/end, metrics, events). Default configuration (DB_POOL_SIZE=200, DB_MAX_OVERFLOW=10) provides 210 total connections, supporting ~35 concurrent traced requests. This is adequate for typical deployments. High-traffic production systems (>50 req/sec sustained) should increase pool size via environment variables: DB_POOL_SIZE=500, DB_MAX_OVERFLOW=100 to support 80+ concurrent requests. Monitor for "QueuePool limit exceeded" errors and adjust pool sizing accordingly. Note: SQLite connections are capped at 50 due to file-based limitations.

Key Environment Variables

Defaults come from mcpgateway/config.py. .env.example intentionally overrides a few for local/dev convenience.

# Core
HOST=127.0.0.1                  # .env.example uses 0.0.0.0
PORT=4444
DATABASE_URL=sqlite:///./mcp.db   # or postgresql+psycopg://...
REDIS_URL=redis://localhost:6379/0
RELOAD=false

# Auth
JWT_SECRET_KEY=your-secret-key
BASIC_AUTH_USER=admin
BASIC_AUTH_PASSWORD=changeme
AUTH_REQUIRED=true                   # Set false ONLY for development
AUTH_ENCRYPTION_SECRET=             # REQUIRED: generate with: make init-secrets-patch-env

# Features
MCPGATEWAY_UI_ENABLED=false          # .env.example sets true
MCPGATEWAY_ADMIN_API_ENABLED=false   # .env.example sets true
MCPGATEWAY_A2A_ENABLED=true
PLUGINS_ENABLED=false
PLUGINS_CONFIG_FILE=plugins/config.yaml

# Logging
LOG_LEVEL=ERROR
LOG_TO_FILE=false
STRUCTURED_LOGGING_DATABASE_ENABLED=false

# Observability
OBSERVABILITY_ENABLED=false
OTEL_EXPORTER_OTLP_ENDPOINT=          # .env.example sets http://localhost:4317

MCP Helpers

# Generate JWT token
python -m mcpgateway.utils.create_jwt_token --username admin@example.com --exp 10080 --secret KEY

# Export for API calls
export MCPGATEWAY_BEARER_TOKEN=$(python -m mcpgateway.utils.create_jwt_token --username admin@example.com --exp 0 --secret KEY)

# Expose stdio server via HTTP/SSE
python -m mcpgateway.translate --stdio "uvx mcp-server-git" --port 9000

Adding an MCP Server

  1. Start: python -m mcpgateway.translate --stdio "server-command" --port 9000
  2. Register: POST /gateways
  3. Create virtual server: POST /servers
  4. Access via SSE/WebSocket endpoints

Technology Stack

  • FastAPI with Pydantic validation and SQLAlchemy ORM (Starlette ASGI)
  • HTMX + Alpine.js for admin UI
  • SQLite default, PostgreSQL support, Redis for caching/federation
  • Alembic for migrations

Synchronous SQLAlchemy in Async Handlers (Design Decision)

The codebase deliberately uses synchronous SQLAlchemy sessions (e.g. SessionLocal().begin()) inside async FastAPI handlers and ASGI middleware, relying on FastAPI's event-loop management to handle blocking operations. Do not flag this as a bug or attempt to convert individual call sites to async without a broader migration plan. This design decision may be revisited in the future alongside a potential migration to async database drivers.

Alembic Database Migrations

When adding new database columns or tables, create an Alembic migration.

Creating Migrations

# CRITICAL: Always check the current head FIRST
cd mcpgateway && alembic heads

# Generate a new migration (auto-generates from model changes)
alembic revision --autogenerate -m "add_column_to_table"

# Or create an empty migration for manual edits
alembic revision -m "add_column_to_table"

Migration File Requirements

The down_revision MUST point to the current head. Never guess or copy from older migrations.

# CORRECT: Points to actual current head (verified via `alembic heads`)
revision: str = "abc123def456"
down_revision: Union[str, Sequence[str], None] = "43c07ed25a24"  # Current head

# WRONG: Creates multiple heads (breaks all tests)
down_revision: Union[str, Sequence[str], None] = "some_old_revision"

Idempotent Migrations Pattern

Always write idempotent migrations that check before modifying:

def upgrade() -> None:
    inspector = sa.inspect(op.get_bind())

    # Skip if table doesn't exist (fresh DB uses db.py models directly)
    if "my_table" not in inspector.get_table_names():
        return

    # Skip if column already exists
    columns = [col["name"] for col in inspector.get_columns("my_table")]
    if "new_column" in columns:
        return

    op.add_column("my_table", sa.Column("new_column", sa.String(), nullable=True))

Verification

# Verify single head after creating migration
cd mcpgateway && alembic heads
# Should show only ONE head

# Run tests to confirm migrations work
make test

Common Errors

  • "Multiple heads are present": Your down_revision points to wrong parent. Fix by updating to actual current head.
  • "Target database is not up to date": Run alembic upgrade head first.

Hermetic Downgrade: Config Snapshot Pattern

Any migration whose downgrade() logic depends on runtime configuration (i.e., reads from mcpgateway.config.settings) must snapshot those values into migration_metadata during upgrade() and read them back during downgrade(). This makes the migration hermetic — its behaviour is determined by database state, not the current environment.

from mcpgateway.config import settings
from sqlalchemy import inspect, text

REVISION = "your_revision_id"

def upgrade() -> None:
    bind = op.get_bind()
    # ... schema changes ...

    # Snapshot any settings values used in downgrade
    if "migration_metadata" in inspect(bind).get_table_names():
        bind.execute(
            text(
                "INSERT INTO migration_metadata (revision, key, value, created_at) "
                "VALUES (:rev, :key, :val, CURRENT_TIMESTAMP) "
                "ON CONFLICT (revision, key) DO UPDATE SET value = excluded.value"
            ),
            {"rev": REVISION, "key": "some_setting", "val": settings.some_setting},
        )

def downgrade() -> None:
    bind = op.get_bind()

    # Read from snapshot; fall back to live settings only if table is absent
    # (pre-existing DB that was upgraded before this pattern was introduced)
    cfg = {}
    if "migration_metadata" in inspect(bind).get_table_names():
        rows = bind.execute(
            text("SELECT key, value FROM migration_metadata WHERE revision = :rev"),
            {"rev": REVISION},
        ).all()
        cfg = {r[0]: r[1] for r in rows}

    some_setting = cfg.get("some_setting") or settings.some_setting

    # ... use some_setting for downgrade logic ...

    # Clean up snapshot rows
    if "migration_metadata" in inspect(bind).get_table_names():
        bind.execute(
            text("DELETE FROM migration_metadata WHERE revision = :rev"),
            {"rev": REVISION},
        )

Rule: If your migration imports settings and uses it in downgrade(), you must follow this pattern. Migrations that only use settings in upgrade() (e.g., for seeding initial data) are exempt.

Coding Standards

  • Python >= 3.11 with type hints; strict mypy
  • Formatting: Ruff (line length 200)
  • Linting: Ruff (E3,E4,E7,E9,F,D1), Pylint per pyproject.toml
  • Naming: snake_case functions/modules, PascalCase classes, UPPER_CASE constants
  • Imports: Group per isort sections (stdlib, third-party, first-party mcpgateway, local)

Commit & PR Standards

  • Sign commits: git commit -s (DCO requirement)
  • Conventional Commits: feat:, fix:, docs:, refactor:, chore:
  • Link issues: Closes #123
  • Include tests for behavior changes
  • Require green lint and tests before PR
  • Don't push until asked.

Tone for GitHub Comments

When posting PR reviews, issue comments, or any public-facing text on GitHub, use a collaborative and constructive tone:

  • Lead with what's good before raising concerns.
  • Frame issues as questions or options ("worth considering", "a couple of approaches") rather than directives.
  • Remember contributors are people doing their jobs — be direct about problems without being harsh.
  • Categorize findings clearly (blocking, suggestions, minor notes) so the author knows what must change vs. what's optional.
  • Avoid sounding algorithmic or robotic; write the way a respectful senior colleague would in a code review.

GitHub Issues (Brief)

  • Prefer issue templates in .github/ISSUE_TEMPLATE/: bug-report-code.md, feature-request.md, docs-issue.md, testing--bug--unit--manual--or-new-test-.md, chore-task--devops--linting--maintenance-.md.
  • Title style should include type prefix, for example: [BUG]: ..., [FEATURE]: ..., [DOCS]: ..., [TESTING]: ..., [CHORE]: ....
  • Label baseline: one primary type label (bug or enhancement or documentation or testing or chore) plus triage on new issues.
  • Add 1-3 optional scope labels as needed (for example security, performance, ui, api, python, devops, a2a, mcp-protocol).
  • Epic title format: [EPIC][SECURITY]: Security clearance levels plugin - Bell-LaPadula MAC implementation #1245.
  • Epic labels: epic, security, enhancement, triage (plus optional scope labels).

Maintenance Guardrails (Brief)

  • Source of truth precedence: mcpgateway/config.py and runtime code > Makefile targets/dependencies > .env.example (dev overrides) > docs/comments.
  • When auditing repo state, prioritize active source directories and ignore transient/workbench content unless explicitly requested: todo/, tmp/, artifacts/, logs/, coverage/.
  • Issue lifecycle labels: use awaiting-user when blocked on reporter feedback, blocked for dependency blockers, planned when accepted but deferred, and fixed only after the resolving change is merged.
  • Avoid brittle numeric claims (counts of services/routers/middleware/plugins) unless you are actively validating and updating them in the same change; otherwise describe with approximate wording.

Important Constraints

  • Never mention AI assistants in PRs/diffs
  • Do not include test plans or effort estimates in PRs
  • Never create files unless absolutely necessary; prefer editing existing files
  • Never proactively create documentation files unless explicitly requested
  • Never commit secrets; use .env for configuration

Key Files

  • README.md - Canonical project overview and quick start
  • mcpgateway/main.py - Application entry point
  • mcpgateway/config.py - Environment configuration
  • mcpgateway/db.py - SQLAlchemy ORM models and session management
  • mcpgateway/schemas.py - Pydantic schemas
  • pyproject.toml - Project configuration
  • Makefile - Build automation
  • .env.example - Environment template

CLI Tools Available

  • gh for GitHub operations
  • make for build/test automation
  • uv for virtual environment management and for uv tool run linter invocations
  • Dev-group tools installed in the venv: pytest, mypy, bandit, pre-commit, etc. (see pyproject.toml [dependency-groups])
  • Formatters and linters (ruff, vulture, interrogate, radon, yamllint, tomlcheck) are pinned in the Makefile and invoked on demand via uv tool run; always prefer the Makefile targets (make black, make ruff, etc.) over calling the underlying tools directly